Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
25 changes: 25 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ WEBAPP_URL=http://localhost:3000
# If your pplication uses a custom base path, specify the route to the API endpoint in full, e.g. NEXTAUTH_URL=https://example.com/custom-route/api/auth
NEXTAUTH_URL=http://localhost:3000

# Optional server-only Better Auth JWKS endpoint for MCP OAuth verification.
# Use this when the runtime cannot reach its public URL; public issuer and redirect URLs remain unchanged.
# MCP_OAUTH_JWKS_URL=http://formbricks:3000/api/auth/jwks

# Can be used to deploy the application under a sub-path of a domain. This can only be set at build time
# BASE_PATH=

Expand Down Expand Up @@ -57,6 +61,27 @@ LOG_LEVEL=info

DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public'

#########################
# AUTHZED / SPICEDB #
#########################
# The local development stack starts a persistent SpiceDB instance backed by
# the bundled PostgreSQL server. pnpm dev:setup generates and preserves both
# secrets below.
AUTHZED_ENABLED=true
AUTHZED_ENDPOINT=localhost:50051
AUTHZED_TOKEN=
AUTHZED_SYSTEM_KEY=formbricks
AUTHZED_INSECURE=true
AUTHZED_CONSISTENCY=minimize_latency
AUTHZED_DATABASE_PASSWORD=

# Optional image and localhost port overrides for docker-compose.dev.yml.
# SPICEDB_IMAGE_REF=authzed/spicedb:v1.52.0
# ZED_IMAGE_REF=authzed/zed:v1.1.1
# GRPCUI_IMAGE_REF=fullstorydev/grpcui:v1.5.2
# SPICEDB_GRPC_PORT=50051
# AUTHZED_GRPCUI_PORT=50052

#################
# HUB (DEV) #
#################
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/api-v3-contract-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ jobs:
# A burst of cases would otherwise trip the limiter and turn most operations into
# documented-but-uninteresting 429s.
echo "RATE_LIMITING_DISABLED=1" >> .env
sed -i "s|AUTHZED_CONSISTENCY=.*|AUTHZED_CONSISTENCY=fully_consistent|" .env
# Not about running Playwright: this is the flag that stops the app sending an instanceId
# alongside the shared licence key (license.ts — "Skip instance ID during E2E tests to avoid
# license key conflicts"). Without it the licence server answers 403 "bound to another
Expand All @@ -133,6 +134,14 @@ jobs:
run: pnpm build --filter=@formbricks/web...
shell: bash

# Keep the fixture offline during the build: a successful build still proves importing the
# authorization runtime does not perform an RPC. Runtime contract requests are authoritative,
# however, so install the canonical schema before projecting the seeded PostgreSQL grants.
- name: Start AuthZed CI fixture
if: steps.harness.outputs.present == 'true'
run: bash scripts/start-authzed-ci.sh
shell: bash

- name: Apply Prisma migrations
if: steps.harness.outputs.present == 'true'
# @formbricks/database is already built by the build step, so run the migration runner
Expand All @@ -156,6 +165,19 @@ jobs:
run: pnpm --filter=@formbricks/database db:seed:contract
shell: bash

- name: Project seeded authorization relationships
if: steps.harness.outputs.present == 'true'
shell: bash
run: |
set +e
pnpm authzed:backfill --apply > /tmp/authzed-backfill.json
backfill_exit=$?
set -e

# Aggregate evidence only: the detailed report contains disposable fixture identifiers.
jq -c '{status, code, counters, truncated}' /tmp/authzed-backfill.json || true
exit "${backfill_exit}"

- name: Run App
if: steps.harness.outputs.present == 'true'
shell: bash
Expand Down
107 changes: 107 additions & 0 deletions .github/workflows/docker-build-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,60 @@ permissions:
contents: read

jobs:
validate-authzed-compose:
name: Validate AuthZed Compose
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
env:
DATABASE_URL: postgresql://test:test@127.0.0.1:5432/formbricks
ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
REDIS_URL: redis://127.0.0.1:6379
CUBEJS_API_URL: http://127.0.0.1:4000
CUBEJS_API_SECRET: build-time-placeholder
HUB_API_URL: http://127.0.0.1:4000
HUB_API_KEY: build-time-placeholder

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
with:
egress-policy: audit

- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Setup Node.js 22.x
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Install dependencies
run: pnpm install --frozen-lockfile --config.platform=linux --config.architecture=x64

- name: Build web workspace dependencies
run: pnpm build --filter=@formbricks/web^...

- name: Test AuthZed client and schema lifecycle
run: pnpm --dir apps/web test lib/authzed

- name: Validate Compose contracts
run: bash docker/authzed-compose-contract.sh

- name: Run AuthZed application and persistence smoke test
run: bash docker/authzed-smoke.sh

validate-docker-build:
name: Validate Docker Build
runs-on: ubuntu-latest
permissions:
contents: read

# Add PostgreSQL and Redis service containers
services:
Expand Down Expand Up @@ -49,12 +100,16 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
persist-credentials: false

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1

- name: Verify Valkey Image Platforms
shell: bash
env:
AUTHZED_DATABASE_PASSWORD: 0000000000000000000000000000000000000000000000000000000000000002
AUTHZED_TOKEN: 0000000000000000000000000000000000000000000000000000000000000001
run: |
set -euo pipefail

Expand Down Expand Up @@ -108,6 +163,56 @@ jobs:
node --version
'

- name: Verify Release-Matched AuthZed Operator CLI
shell: bash
env:
GITHUB_SHA: ${{ github.sha }}
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
run: |
set -euo pipefail

IMAGE="formbricks-test:$GITHUB_SHA"
docker run --rm --entrypoint sh "$IMAGE" -c '
set -eu
test "$(id -u)" != "0"
test -x /usr/local/bin/formbricks-authzed
test -r /home/nextjs/authzed-cli/index.mjs
test -r /home/nextjs/authzed-cli/schema.zed
'

AUTHZED_CLI_ENV=(
-e DATABASE_URL="postgresql://test:test@192.0.2.1:5432/formbricks"
-e ENCRYPTION_KEY="$DUMMY_ENCRYPTION_KEY"
-e REDIS_URL="redis://192.0.2.1:6379"
-e HUB_API_URL="http://192.0.2.1:4000"
-e HUB_API_KEY="build-time-placeholder"
-e CUBEJS_API_URL="http://192.0.2.1:4000"
-e CUBEJS_API_SECRET="build-time-placeholder"
-e AUTHZED_ENABLED="false"
)

set +e
output="$(docker run --rm \
--entrypoint formbricks-authzed \
"${AUTHZED_CLI_ENV[@]}" \
"$IMAGE" health 2>&1)"
status=$?
set -e

test "$status" -eq 1
test "$output" = '{"status":"disabled"}'

set +e
upgrade_output="$(docker run --rm \
--entrypoint formbricks-authzed \
"${AUTHZED_CLI_ENV[@]}" \
"$IMAGE" upgrade check 2>&1)"
upgrade_status=$?
set -e

test "$upgrade_status" -eq 1
test "$upgrade_output" = '{"code":"authzed_disabled","retryable":false,"status":"failed"}'

- name: Reject Invalid Environment Before Database Setup
shell: bash
env:
Expand Down Expand Up @@ -221,6 +326,8 @@ jobs:
"BETTER_AUTH_SECRET=$DUMMY_ENCRYPTION_KEY" \
'HUB_API_KEY=build-time-placeholder' \
'CUBEJS_API_SECRET=build-time-placeholder' \
'AUTHZED_TOKEN=compose-authzed-token-placeholder' \
'AUTHZED_DATABASE_PASSWORD=compose-authzed-database-placeholder' \
'AI_PROVIDER=compose-provider-secret-sentinel' \
> docker/.env

Expand Down
88 changes: 87 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ jobs:
run: |
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${{ secrets.ENTERPRISE_LICENSE_KEY }}/" .env
sed -i "s|REDIS_URL=.*|REDIS_URL=redis://localhost:6379|" .env
sed -i "s|AUTHZED_CONSISTENCY=.*|AUTHZED_CONSISTENCY=fully_consistent|" .env
echo "" >> .env
echo "E2E_TESTING=1" >> .env
echo "S3_REGION=us-east-1" >> .env
Expand All @@ -142,6 +143,10 @@ jobs:
echo "S3_FORCE_PATH_STYLE=1" >> .env
shell: bash

- name: Start AuthZed CI fixture
run: bash scripts/start-authzed-ci.sh
shell: bash

- name: Start RustFS Server
run: |
set -euo pipefail
Expand Down Expand Up @@ -343,14 +348,48 @@ jobs:
sleep 10
done

# Playwright fixtures seed authorization source rows directly through Prisma. That intentionally
# bypasses product services, so run the real durable delivery processor at a test-only cadence
# rather than duplicating relationship writes in fixture code or waiting for the 5-second schedule.
- name: Start AuthZed CI outbox worker
id: authzed-worker-start
run: |
heartbeat_path="${RUNNER_TEMP}/formbricks-authzed-outbox-worker.heartbeat"
rm -f "${heartbeat_path}"
cd apps/web
AUTHZED_CI_OUTBOX_HEARTBEAT_PATH="${heartbeat_path}" \
node --env-file=../../.env --conditions=react-server --import tsx \
scripts/authzed-ci-outbox-worker.ts \
> ../../authzed-outbox.log 2>&1 &
worker_pid="$!"
cd ../..
echo "AUTHZED_CI_OUTBOX_HEARTBEAT_PATH=${heartbeat_path}" >> "$GITHUB_ENV"
echo "AUTHZED_CI_OUTBOX_PID=${worker_pid}" >> "$GITHUB_ENV"

for attempt in {1..600}; do
if ! kill -0 "${worker_pid}" 2>/dev/null; then
echo "AuthZed CI outbox worker exited before becoming ready"
exit 1
fi
if [ -f "${heartbeat_path}" ]; then
exit 0
fi
sleep 0.1
done

echo "AuthZed CI outbox worker did not publish its readiness heartbeat after ${attempt} attempts"
exit 1
shell: bash

- name: Run E2E Tests (Playwright Service)
if: env.PW_MODE == 'service'
timeout-minutes: 15
env:
PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }}
PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }}
CI: true
run: pnpm test-e2e:azure
run: |
pnpm test-e2e:azure

- name: Run E2E Tests (Local)
if: env.PW_MODE == 'local'
Expand All @@ -360,6 +399,44 @@ jobs:
run: |
pnpm test:e2e

- name: Verify AuthZed CI outbox worker
if: always() && steps.authzed-worker-start.outcome == 'success'
run: |
status=0
worker_pid="${AUTHZED_CI_OUTBOX_PID:-}"
heartbeat_path="${AUTHZED_CI_OUTBOX_HEARTBEAT_PATH:-}"

if [ -z "${worker_pid}" ] || ! kill -0 "${worker_pid}" 2>/dev/null; then
echo "AuthZed CI outbox worker is not running"
status=1
else
worker_command="$(ps -p "${worker_pid}" -o command= || true)"
if [[ "${worker_command}" != *"authzed-ci-outbox-worker.ts"* ]]; then
echo "AuthZed CI outbox worker PID no longer identifies the expected process"
status=1
fi
fi

if [ -z "${heartbeat_path}" ] || [ ! -f "${heartbeat_path}" ]; then
echo "AuthZed CI outbox worker heartbeat is missing"
status=1
else
heartbeat_age="$(( $(date +%s) - $(stat -c %Y "${heartbeat_path}") ))"
# The worker emits a heartbeat every 100 ms. Allow 100 missed heartbeats so a loaded
# runner is not mistaken for a dead worker while still catching a stalled loop.
if [ "${heartbeat_age}" -gt 10 ]; then
echo "AuthZed CI outbox worker heartbeat is stale"
status=1
fi
fi

if [ -n "${worker_pid}" ]; then
kill "${worker_pid}" 2>/dev/null || true
fi

exit "${status}"
shell: bash

- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
if: always()
with:
Expand All @@ -382,3 +459,12 @@ jobs:
else
echo "app.log not found because the Run App step did not execute or failed before log creation."
fi

- name: Output AuthZed logs
if: failure()
run: |
docker logs formbricks-authzed-ci 2>&1 || true
if [ -f authzed-outbox.log ]; then
cat authzed-outbox.log
fi
shell: bash
Loading
Loading