diff --git a/test/e2e/README.md b/test/e2e/README.md index 0821c5143..71c212cba 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -11,6 +11,10 @@ | `--port` | | Override URL port (useful for local) | | `--test` | | Comma-separated list of test categories to run (runs all if omitted) | | `--json` | | Output results as JSON to stdout (all other output goes to stderr) | +| `--url` | | Override project URL (e.g. `http://127.0.0.1:54321`) | +| `--db-url` | | Override database URL (e.g. `postgresql://postgres:postgres@127.0.0.1:54322/postgres`) | +| `--otel` | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP HTTP endpoint for tracing (e.g. `http://localhost:4318`) | +| | `OTEL_API_TOKEN` | Bearer token for authenticated OTLP endpoints | Sensitive credentials (`--secret-key`, `SUPABASE_DB_PASSWORD`) should be set as environment variables to avoid them appearing in shell history. @@ -18,21 +22,32 @@ A random test user is created at the start of each run and deleted automatically ## Test categories -Pass any combination to `--test` as a comma-separated list: +Pass any combination to `--test` as a comma-separated list. Use `functional` to run all non-load suites, or `load` to run all load suites. -| Category | Description | -|---|---| -| `connection` | WebSocket connect latency and broadcast throughput | -| `load` | Postgres changes and presence throughput (INSERT / UPDATE / DELETE) | -| `broadcast` | Self-broadcast and REST broadcast API | -| `presence` | Presence join on public and private channels | -| `authorization` | Private channel allow/deny checks | -| `postgres-changes` | Filtered INSERT, UPDATE, DELETE events and concurrent changes | -| `broadcast-changes` | Database-triggered broadcast INSERT, UPDATE, DELETE events | +| Category | Suites | Tests | +|---|---|---| +| `connection` | connection | First connect latency; broadcast message throughput | +| `load` | load-postgres-changes | Postgres system message latency; INSERT / UPDATE / DELETE throughput via postgres changes | +| | load-presence | Presence join throughput | +| | load-broadcast-from-db | Broadcast-from-database throughput | +| | load-broadcast | Self-broadcast throughput; REST broadcast API throughput | +| | load-broadcast-replay | Broadcast replay throughput on channel join | +| `broadcast` | broadcast extension | Self-broadcast receive; REST broadcast API send-and-receive | +| `presence` | presence extension | Presence join on public channels; presence join on private channels | +| `authorization` | authorization check | Private channel denied without permissions; private channel allowed with permissions | +| `postgres-changes` | postgres changes extension | Filtered INSERT, UPDATE, DELETE events; concurrent INSERT + UPDATE + DELETE | +| `broadcast-changes` | broadcast changes | DB-triggered broadcast for INSERT, UPDATE, DELETE | +| `broadcast-replay` | broadcast replay | Replayed messages delivered on join; `meta.replayed` flag set; messages before `since` not replayed | ```bash # Run only connection and broadcast tests ./realtime-check --env local --publishable-key --secret-key --test connection,broadcast + +# Run all load tests +./realtime-check --env local --publishable-key --secret-key --test load + +# Run all functional (non-load) tests +./realtime-check --env local --publishable-key --secret-key --test functional ``` ## JSON output @@ -51,12 +66,31 @@ The pre-built binary requires no runtime — just run it directly. ### Local project +A `supabase/config.toml` is included, so `supabase start` works out of the box. + ```bash supabase start SUPABASE_SERVICE_ROLE_KEY= \ ./realtime-check --env local --publishable-key ``` +### Local project with tracing + +```bash +supabase start +docker compose up -d # starts Jaeger at http://localhost:16686 +SUPABASE_SERVICE_ROLE_KEY= \ + ./realtime-check --env local --publishable-key --otel http://localhost:4318 +``` + +For authenticated OTLP endpoints, set `OTEL_API_TOKEN` and it will be sent as a `Bearer` token: + +```bash +SUPABASE_SERVICE_ROLE_KEY= \ +OTEL_API_TOKEN= \ + ./realtime-check --env local --publishable-key --otel https://otlp.example.com +``` + ### Remote project ```bash @@ -102,7 +136,7 @@ SUPABASE_SERVICE_ROLE_KEY= SUPABASE_DB_PASSWORD= \ ./result/bin/realtime-check --project --publishable-key ``` -> **Note:** The nix build locks the dependency hash in `flake.nix`. If you update `package.json` or `bun.lock`, run `nix build` once — it will fail with the new hash in the error output — then update `outputHash` in `flake.nix` accordingly. +`bun run nix` calls `nix-build.sh`, which automatically updates the `outputHash` in `flake.nix` when `package.json` or `bun.lock` change — no manual hash update needed. --- diff --git a/test/e2e/bun.lock b/test/e2e/bun.lock index 949ad9fa7..0de0d5223 100644 --- a/test/e2e/bun.lock +++ b/test/e2e/bun.lock @@ -5,6 +5,12 @@ "": { "name": "realtime-check", "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.6.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", + "@opentelemetry/resources": "^2.6.0", + "@opentelemetry/sdk-trace-base": "^2.6.0", + "@opentelemetry/semantic-conventions": "^1.40.0", "@supabase/supabase-js": "latest", "cli-table3": "^0.6.5", "commander": "^12.1.0", @@ -15,6 +21,50 @@ "packages": { "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.213.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.213.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/otlp-exporter-base": "0.213.0", "@opentelemetry/otlp-transformer": "0.213.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/sdk-trace-base": "2.6.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-tnRmJD39aWrE/Sp7F6AbRNAjKHToDkAqBi6i0lESpGWz3G+f4bhVAV6mgSXH2o18lrDVJXo6jf9bAywQw43wRA=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.213.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/otlp-transformer": "0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MegxAP1/n09Ob2dQvY5NBDVjAFkZRuKtWKxYev1R2M8hrsgXzQGkaMgoEKeUOyQ0FUyYcO29UOnYdQWmWa0PXg=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.213.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.213.0", "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/sdk-logs": "0.213.0", "@opentelemetry/sdk-metrics": "2.6.0", "@opentelemetry/sdk-trace-base": "2.6.0", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RSuAlxFFPjeK4d5Y6ps8L2WhaQI6CXWllIjvo5nkAlBpmq2XdYWEBGiAbOF4nDs8CX4QblJDv5BbMUft3sEfDw=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.213.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.213.0", "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-00xlU3GZXo3kXKve4DLdrAL0NAFUaZ9appU/mn00S/5kSUdAvyYsORaDUfR04Mp2CLagAOhrzfUvYozY/EZX2g=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@supabase/auth-js": ["@supabase/auth-js@2.98.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg=="], "@supabase/functions-js": ["@supabase/functions-js@2.98.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg=="], @@ -47,6 +97,10 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/test/e2e/docker-compose.yml b/test/e2e/docker-compose.yml new file mode 100644 index 000000000..8f33b4e4b --- /dev/null +++ b/test/e2e/docker-compose.yml @@ -0,0 +1,11 @@ +# E2e testing infrastructure. Requires `supabase start` to be running first. +# Run from test/e2e: +# docker compose up -d +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 bun run realtime-check.ts --env local +services: + jaeger: + image: jaegertracing/jaeger:2.5.0 + container_name: e2e-jaeger + ports: + - "16686:16686" + - "4318:4318" diff --git a/test/e2e/flake.nix b/test/e2e/flake.nix index a4e60db07..11f39c37e 100644 --- a/test/e2e/flake.nix +++ b/test/e2e/flake.nix @@ -30,7 +30,7 @@ installPhase = "cp -r node_modules $out"; outputHashMode = "recursive"; outputHashAlgo = "sha256"; - outputHash = "sha256-MK55AYy2z5nY7B30o8vt34+wk+86Zruz/q2ZDmA951c="; + outputHash = "sha256-I7ZNZkyK83Lk+Ut3j6FngvWNfIl8JaW2cF4bfyVf5TQ="; }; in { packages.default = pkgs.stdenv.mkDerivation { diff --git a/test/e2e/nix-build.sh b/test/e2e/nix-build.sh new file mode 100755 index 000000000..6e1ec8284 --- /dev/null +++ b/test/e2e/nix-build.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +FLAKE="flake.nix" +FAKE_HASH="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + +# Replace current hash with fake to force nix to reveal the correct one +sed -i.bak "s|outputHash = \"sha256-.*\";|outputHash = \"${FAKE_HASH}\";|" "$FLAKE" +rm -f "${FLAKE}.bak" + +echo "Probing for correct node_modules hash..." +NIX_OUT=$(nix build 2>&1 || true) + +REAL_HASH=$(echo "$NIX_OUT" | grep "got:" | awk '{print $2}') + +if [[ -z "$REAL_HASH" ]]; then + # No hash mismatch error — either build succeeded or failed for another reason + if echo "$NIX_OUT" | grep -q "error:"; then + echo "Build failed:" + echo "$NIX_OUT" + git checkout -- "$FLAKE" 2>/dev/null || true + exit 1 + else + echo "Hash was already correct. Build succeeded." + echo "Done. Binary available at ./result/bin/realtime-check" + exit 0 + fi +fi + +echo "Updating hash to: $REAL_HASH" +sed -i.bak "s|outputHash = \"${FAKE_HASH}\";|outputHash = \"${REAL_HASH}\";|" "$FLAKE" +rm -f "${FLAKE}.bak" + +echo "Building with correct hash..." +nix build +echo "Done. Binary available at ./result/bin/realtime-check" diff --git a/test/e2e/package.json b/test/e2e/package.json index 11a53aa30..ffcf54302 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -2,6 +2,12 @@ "name": "realtime-check", "version": "0.0.1", "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.6.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", + "@opentelemetry/resources": "^2.6.0", + "@opentelemetry/sdk-trace-base": "^2.6.0", + "@opentelemetry/semantic-conventions": "^1.40.0", "@supabase/supabase-js": "latest", "cli-table3": "^0.6.5", "commander": "^12.1.0", @@ -10,6 +16,6 @@ "scripts": { "check": "bun run realtime-check.ts --", "build": "bun build --compile --minify-syntax --minify-whitespace --minify-identifiers realtime-check.ts --outfile realtime-check", - "nix": "bun run build && nix build" + "nix": "bun install && bun run build && bash nix-build.sh" } } diff --git a/test/e2e/realtime-check.ts b/test/e2e/realtime-check.ts index 35061fd8f..22b5e2e2f 100644 --- a/test/e2e/realtime-check.ts +++ b/test/e2e/realtime-check.ts @@ -5,6 +5,12 @@ import { Command } from "commander"; import kleur from "kleur"; import { SQL } from "bun"; import Table from "cli-table3"; +import { trace, context, SpanStatusCode, SpanKind, ROOT_CONTEXT } from "@opentelemetry/api"; +import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; const program = new Command() .name("realtime-check") @@ -16,7 +22,10 @@ const program = new Command() .option("--env ", "Environment: local | staging | prod (default: prod)", "prod") .option("--domain ", "Email domain for the test user", "example.com") .option("--port ", "Override URL port (useful for local)") + .option("--url ", "Override project URL (e.g. http://127.0.0.1:54321)") + .option("--db-url ", "Override database URL (e.g. postgresql://postgres:postgres@127.0.0.1:54322/postgres)") .option("--json", "Output results as JSON to stdout") + .option("--otel ", "OTLP HTTP endpoint for tracing (e.g. http://localhost:4318)") .option("--test ", "Comma-separated list of test categories to run: functional,load,connection,load-postgres-changes,load-presence,load-broadcast,load-broadcast-from-db,load-broadcast-replay,broadcast,broadcast-replay,presence,authorization,postgres-changes,broadcast-changes") .parse(); @@ -24,7 +33,7 @@ const opts = program.opts(); const ANON_KEY: string = opts.publishableKey ?? process.env.SUPABASE_ANON_KEY; const SERVICE_KEY: string = opts.secretKey ?? process.env.SUPABASE_SERVICE_ROLE_KEY; const dbPassword: string = opts.dbPassword ?? process.env.SUPABASE_DB_PASSWORD ?? ""; -const { project, env, domain: EMAIL_DOMAIN, port, json: JSON_OUTPUT, test: TEST_FILTER } = opts; +const { project, env, domain: EMAIL_DOMAIN, port, json: JSON_OUTPUT, test: TEST_FILTER, otel: OTEL_ARG, url: URL_ARG, dbUrl: DB_URL_ARG } = opts; const TEST_CATEGORIES = TEST_FILTER ? TEST_FILTER.split(",").map((s: string) => s.trim().toLowerCase()) @@ -47,13 +56,13 @@ if (!SERVICE_KEY) { process.exit(1); } -const PROJECT_URL = (() => { +const PROJECT_URL = URL_ARG ?? process.env.SUPABASE_URL ?? (() => { if (env === "local") return `http://localhost:${port ?? 54321}`; if (env === "staging") return `https://${project}.green.supabase.co`; return `https://${project}.supabase.co`; })(); -const DB_URL = (() => { +const DB_URL = DB_URL_ARG ?? process.env.SUPABASE_DB_URL ?? (() => { const pw = encodeURIComponent(dbPassword ?? "postgres"); if (env === "local") return `postgresql://postgres:${pw}@localhost:${port ?? 54322}/postgres`; if (env === "staging") return `postgresql://postgres:${pw}@db.${project}.green.supabase.co:5432/postgres`; @@ -75,7 +84,68 @@ const LOAD_MESSAGES = 20; const LOAD_SETTLE_MS = 5000; const LOAD_DELIVERY_SLO = 99; +const OTEL_ENDPOINT = OTEL_ARG ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT; +const OTEL_API_TOKEN = process.env.OTEL_API_TOKEN; + +let tracer = trace.getTracer("realtime-check"); +let otelProvider: BasicTracerProvider | null = null; + +function initOtel() { + if (!OTEL_ENDPOINT) return; + const contextManager = new AsyncLocalStorageContextManager(); + contextManager.enable(); + context.setGlobalContextManager(contextManager); + const provider = new BasicTracerProvider({ + resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "realtime-check" }), + spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ + url: `${OTEL_ENDPOINT}/v1/traces`, + ...(OTEL_API_TOKEN ? { headers: { Authorization: `Bearer ${OTEL_API_TOKEN}` } } : {}), + }))], + }); + trace.setGlobalTracerProvider(provider); + tracer = trace.getTracer("realtime-check", "0.0.1"); + otelProvider = provider; +} + +async function flushOtel() { + if (otelProvider) await otelProvider.forceFlush(); +} + +function patchFetch() { + if (!OTEL_ENDPOINT) return; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async function tracedFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("/rest/v1") || url.includes("/auth/v1/logout") || url.includes("/auth/v1/admin")) return originalFetch(input, init); + const method = (init?.method ?? (typeof input === "object" && "method" in input ? input.method : undefined) ?? "GET").toUpperCase(); + const span = tracer.startSpan(`HTTP ${method}`, { + kind: SpanKind.CLIENT, + attributes: { "http.method": method, "http.url": url }, + }, context.active()); + return context.with(trace.setSpan(context.active(), span), async () => { + try { + const res = await originalFetch(input, init); + span.setAttribute("http.status_code", res.status); + if (res.status >= 400) span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${res.status}` }); + return res; + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + span.setStatus({ code: SpanStatusCode.ERROR, message: msg }); + if (e instanceof Error) span.recordException(e); + throw e; + } finally { + span.end(); + } + }); + }) as typeof fetch; +} + + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +const settle = async (getCount: () => number, expected: number, timeoutMs: number) => { + const deadline = performance.now() + timeoutMs; + while (getCount() < expected && performance.now() < deadline) await sleep(50); +}; const log = (...args: unknown[]) => JSON_OUTPUT ? process.stderr.write(args.map(String).join(" ") + "\n") : console.log(...args); const progress = (msg: string) => JSON_OUTPUT ? process.stderr.write(msg) : process.stdout.write(msg); @@ -102,17 +172,28 @@ const results: TestResult[] = []; async function test(name: string, fn: () => Promise) { progress(` ${name} ... `); const start = performance.now(); + const span = tracer.startSpan(name, { + kind: SpanKind.INTERNAL, + attributes: { "suite": currentSuite, "env": env, "project.url": PROJECT_URL }, + }); + const testContext = trace.setSpan(ROOT_CONTEXT, span); try { - const metrics = await fn(); + const metrics = await context.with(testContext, fn); const durationMs = performance.now() - start; + for (const m of metrics) span.setAttribute(`metric.${m.label}`, `${m.value.toFixed(2)}${m.unit}`); + span.setStatus({ code: SpanStatusCode.OK }); results.push({ suite: currentSuite, name, passed: true, durationMs, metrics }); const summary = metrics.map((m) => `${m.label}: ${kleur.cyan(`${m.value.toFixed(m.unit === "%" ? 1 : 0)}${m.unit}`)}`).join(" "); log(`${kleur.green("PASS")} ${kleur.dim(`${durationMs.toFixed(0)}ms`)}${summary ? " " + summary : ""}`); } catch (e: any) { const durationMs = performance.now() - start; + span.setStatus({ code: SpanStatusCode.ERROR, message: e?.message ?? String(e) }); + span.recordException(e); results.push({ suite: currentSuite, name, passed: false, durationMs, metrics: [], error: e?.message ?? String(e) }); log(`${kleur.red("FAIL")} ${kleur.dim(`${durationMs.toFixed(0)}ms`)}`); log(` ${kleur.red(e?.message ?? e)}`); + } finally { + span.end(); } } @@ -122,12 +203,23 @@ function suite(name: string) { } async function waitFor(getter: () => T | null, label: string): Promise<{ value: T; latencyMs: number }> { + const span = tracer.startSpan(`wait: ${label}`, { kind: SpanKind.INTERNAL }); const start = performance.now(); const deadline = start + EVENT_TIMEOUT_MS; let value: T | null; - while ((value = getter()) === null && performance.now() < deadline) await sleep(50); - if (value === null) throw new Error(`Timed out waiting for ${label}`); - return { value, latencyMs: performance.now() - start }; + return context.with(trace.setSpan(context.active(), span), async () => { + while ((value = getter()) === null && performance.now() < deadline) await sleep(50); + const latencyMs = performance.now() - start; + if (value === null) { + span.setStatus({ code: SpanStatusCode.ERROR, message: `Timed out` }); + span.end(); + throw new Error(`Timed out waiting for ${label}`); + } + span.setAttribute("latency_ms", latencyMs); + span.setStatus({ code: SpanStatusCode.OK }); + span.end(); + return { value, latencyMs }; + }); } async function stopClient(supabase: SupabaseClient) { @@ -136,17 +228,37 @@ async function stopClient(supabase: SupabaseClient) { } async function signInUser(supabase: SupabaseClient, email: string, password: string) { - const { data, error } = await supabase.auth.signInWithPassword({ email, password }); - if (error) throw new Error(`Error signing in: ${error.message}`); - return data!.session!.access_token; + const span = tracer.startSpan("sign in", { kind: SpanKind.INTERNAL }); + return context.with(trace.setSpan(context.active(), span), async () => { + const { data, error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) { + span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); + span.end(); + throw new Error(`Error signing in: ${error.message}`); + } + span.setStatus({ code: SpanStatusCode.OK }); + span.end(); + return data!.session!.access_token; + }); } async function waitForSubscribed(channel: ReturnType): Promise { + const span = tracer.startSpan("wait: subscribe", { kind: SpanKind.INTERNAL }); const start = performance.now(); const deadline = start + EVENT_TIMEOUT_MS; - while (channel.state === "joining" && performance.now() < deadline) await sleep(50); - if (channel.state !== "joined") throw new Error(`Channel failed to subscribe (state: ${channel.state})`); - return performance.now() - start; + return context.with(trace.setSpan(context.active(), span), async () => { + while (channel.state === "joining" && performance.now() < deadline) await sleep(50); + const latencyMs = performance.now() - start; + if (channel.state !== "joined") { + span.setStatus({ code: SpanStatusCode.ERROR, message: `state: ${channel.state}` }); + span.end(); + throw new Error(`Channel failed to subscribe (state: ${channel.state})`); + } + span.setAttribute("latency_ms", latencyMs); + span.setStatus({ code: SpanStatusCode.OK }); + span.end(); + return latencyMs; + }); } async function waitForPostgresChannel(channel: ReturnType): Promise<{ subscribeMs: number; systemMs: number }> { @@ -357,7 +469,7 @@ async function runConnectionTest() { await channel.send({ type: "broadcast", event, payload: { seq: i } }); } - await sleep(SETTLE_MS); + await settle(() => latencies.length, MESSAGES, SETTLE_MS); return measureThroughput(latencies, MESSAGES, "messages", DELIVERY_SLO); } finally { @@ -409,7 +521,7 @@ async function runLoadPostgresChangesTests(testUser: { email: string; password: sendTimes.set(id, t); } - await sleep(LOAD_SETTLE_MS); + await settle(() => latencies.length, LOAD_MESSAGES, LOAD_SETTLE_MS); return measureThroughput(latencies, LOAD_MESSAGES, "INSERT events", LOAD_DELIVERY_SLO); } finally { @@ -442,7 +554,7 @@ async function runLoadPostgresChangesTests(testUser: { email: string; password: return executeUpdate(supabase, "pg_changes", id); })); - await sleep(LOAD_SETTLE_MS); + await settle(() => latencies.length, LOAD_MESSAGES, LOAD_SETTLE_MS); return measureThroughput(latencies, LOAD_MESSAGES, "UPDATE events", LOAD_DELIVERY_SLO); } finally { @@ -475,7 +587,7 @@ async function runLoadPostgresChangesTests(testUser: { email: string; password: return executeDelete(supabase, "pg_changes", id); })); - await sleep(LOAD_SETTLE_MS); + await settle(() => latencies.length, LOAD_MESSAGES, LOAD_SETTLE_MS); return measureThroughput(latencies, LOAD_MESSAGES, "DELETE events", LOAD_DELIVERY_SLO); } finally { @@ -524,7 +636,7 @@ async function runLoadPresenceTests() { return ch.track({ key }); })); - await sleep(LOAD_SETTLE_MS); + await settle(() => latencies.length, CLIENTS, LOAD_SETTLE_MS); return measureThroughput(latencies, CLIENTS, "presence joins", LOAD_DELIVERY_SLO); } finally { @@ -561,7 +673,7 @@ async function runLoadBroadcastFromDbTests(testUser: { email: string; password: await supabase.from("broadcast_changes").insert({ id, value: crypto.randomUUID() }); })); - await sleep(LOAD_SETTLE_MS); + await settle(() => latencies.length, LOAD_MESSAGES, LOAD_SETTLE_MS); await supabase.from("broadcast_changes").delete().in("id", [...sendTimes.keys()]); @@ -599,7 +711,7 @@ async function runLoadBroadcastTests() { await channel.send({ type: "broadcast", event, payload: { seq: i } }); } - await sleep(LOAD_SETTLE_MS); + await settle(() => latencies.length, LOAD_MESSAGES, LOAD_SETTLE_MS); return measureThroughput(latencies, LOAD_MESSAGES, "broadcast events", LOAD_DELIVERY_SLO); } finally { @@ -636,7 +748,7 @@ async function runLoadBroadcastTests() { if (!res.ok) throw new Error(`Broadcast API returned ${res.status}`); })); - await sleep(LOAD_SETTLE_MS); + await settle(() => latencies.length, LOAD_MESSAGES, LOAD_SETTLE_MS); return measureThroughput(latencies, LOAD_MESSAGES, "broadcast API events", LOAD_DELIVERY_SLO); } finally { @@ -661,8 +773,6 @@ async function runLoadBroadcastReplayTests(testUser: { email: string; password: supabase.from("replay_check").insert({ id: crypto.randomUUID(), topic, event, payload: { seq: i } }) )); - await sleep(LOAD_SETTLE_MS); - const latencies: number[] = []; const replayStart = performance.now(); const receiver = supabase.channel(topic, { @@ -672,7 +782,7 @@ async function runLoadBroadcastReplayTests(testUser: { email: string; password: }).subscribe(); await waitForSubscribed(receiver); - await sleep(LOAD_SETTLE_MS); + await settle(() => latencies.length, LOAD_MESSAGES, LOAD_SETTLE_MS); return measureThroughput(latencies, LOAD_MESSAGES, "replayed broadcast events", LOAD_DELIVERY_SLO); } finally { @@ -1160,7 +1270,6 @@ async function runBroadcastReplayTests(testUser: { email: string; password: stri await supabase.from("replay_check").insert({ id: crypto.randomUUID(), topic, event, payload: { value: "old" } }); - await sleep(1000); const since = Date.now(); let result: any = null; @@ -1169,7 +1278,7 @@ async function runBroadcastReplayTests(testUser: { email: string; password: stri }).on("broadcast", { event }, (msg) => (result = msg.payload)).subscribe(); await waitForSubscribed(receiver); - await sleep(2000); + await sleep(500); assert.strictEqual(result, null); return []; @@ -1272,9 +1381,13 @@ const LOAD_SUITES = Object.keys(SUITES).filter((k) => k.startsWith("load")); const FUNCTIONAL_SUITES = Object.keys(SUITES).filter((k) => !k.startsWith("load")); async function main() { + initOtel(); + patchFetch(); + log(kleur.bold("Realtime Check")); log(`Project: ${PROJECT_URL}`); log(`Env: ${env} Email domain: ${EMAIL_DOMAIN}\n`); + if (OTEL_ENDPOINT) log(kleur.dim(`Tracing → ${OTEL_ENDPOINT}\n`)); const activeCategories = TEST_CATEGORIES ? TEST_CATEGORIES.flatMap((c: string) => { @@ -1299,6 +1412,7 @@ async function main() { : Object.entries(SUITES); const { userId, testUser } = await setup(); + const start = performance.now(); try { for (const [, fn] of suitesToRun) await fn(testUser); @@ -1307,6 +1421,7 @@ async function main() { } printSummary(performance.now() - start); + await flushOtel(); if (results.some((r) => !r.passed)) process.exit(1); } diff --git a/test/e2e/supabase/.gitignore b/test/e2e/supabase/.gitignore new file mode 100644 index 000000000..ad9264f0b --- /dev/null +++ b/test/e2e/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/test/e2e/supabase/.temp/cli-latest b/test/e2e/supabase/.temp/cli-latest index 1dd617870..0cce2dc46 100644 --- a/test/e2e/supabase/.temp/cli-latest +++ b/test/e2e/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.75.0 \ No newline at end of file +v2.78.1 \ No newline at end of file diff --git a/test/e2e/supabase/config.toml b/test/e2e/supabase/config.toml new file mode 100644 index 000000000..cfbb86938 --- /dev/null +++ b/test/e2e/supabase/config.toml @@ -0,0 +1,388 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "e2e" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +# This feature is only available on the hosted platform. +[storage.vector] +enabled = false +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1). +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)"