Skip to main content

Org-creation flow probe

Scope: testing POST /portal/onboarding/create-organization end-to-end without the portal UI. Uses curl + CLERK_SECRET_KEY directly against the Clerk Backend API (no clerk CLI login needed — the CLI session is often expired). The point of steps 4–5 is the FB-1 invariant: the owner role is stamped synchronously, so the caller's first org-scoped token already carries it (the old flow 403'd role-gated writes for ~60s).

Source-checked against optolink-backend @ 29f8589, 2026-10-06 (portal-onboarding.service.createOrganization: 409-if-membered → Clerk create with createdBy → mirror → stamp publicMetadata.role='owner', all before the response; response {clerkOrgId, name}; PATCH /portal/app-config/:platform is @Roles('developer')). The Clerk Backend API form-encoding behaviors in step 1 are recorded 2026-09-03; re-verify live on next use.

cd optolink-backend && set -a && . ./.env && set +a && TS=$(date +%s)

# 1. fresh orgless user (⚠ form-encoded: JSON with ARRAY values is rejected;
# plain-string JSON also works, arrays don't)
U=$(curl -s -X POST https://api.clerk.com/v1/users \
-H "Authorization: Bearer $CLERK_SECRET_KEY" \
--data-urlencode "[email protected]" \
--data-urlencode "password=Xq7-$(openssl rand -hex 4)!Aa" \
| jq -r .id)

# 2. session + ORGLESS token (no organization_id)
SID=$(curl -s -X POST https://api.clerk.com/v1/sessions \
-H "Authorization: Bearer $CLERK_SECRET_KEY" -H 'Content-Type: application/json' \
-d "{\"user_id\":\"$U\"}" | jq -r .id)
T=$(curl -s -X POST "https://api.clerk.com/v1/sessions/$SID/tokens" \
-H "Authorization: Bearer $CLERK_SECRET_KEY" -H 'Content-Type: application/json' \
-d '{}' | jq -r .jwt)

# 3. create → 201 {clerkOrgId, name}
O=$(curl -s -X POST localhost:3000/portal/onboarding/create-organization \
-H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"name":"RT Org $TS"}' | jq -r .clerkOrgId)

# 4. THE FB-1 CHECK: mint the org token from a session created AFTER the org
# exists — an older session's token carries no/stale org claims
SID2=$(curl -s -X POST https://api.clerk.com/v1/sessions \
-H "Authorization: Bearer $CLERK_SECRET_KEY" -H 'Content-Type: application/json' \
-d "{\"user_id\":\"$U\"}" | jq -r .id)
OT=$(curl -s -X POST "https://api.clerk.com/v1/sessions/$SID2/tokens" \
-H "Authorization: Bearer $CLERK_SECRET_KEY" -H 'Content-Type: application/json' \
-d "{\"organization_id\":\"$O\"}" | jq -r .jwt)
echo "$OT" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.org_metadata'
# expect {"role":"owner"} — role stamped BEFORE the first org-scoped token

# 5. immediate role-gated write (the old flow 403'd here for ~60s)
curl -s -w '\n%{http_code}\n' -X PATCH localhost:3000/portal/app-config/IOS \
-H "Authorization: Bearer $OT" -H 'Content-Type: application/json' \
-d '{"bundleId":"com.rt.test","storeUrl":"https://apps.apple.com/app/id1"}'
# expect 200

# 6. cleanup — see cleanup.md. Note: single-value list endpoints return a BARE
# array (no {data} wrapper) — `jq '.[0]'`, not `jq '.data[0]'`

The FB-1 semantics themselves (why the stamp is synchronous, what the webhooks confirm) are on auth-clerk, "Self-registration path".