Local Dev Recipes
Common local development setups for Shell host and federated remotes.
Audience: Shell and federated app developers
Prerequisites: Control Plane API running, Keycloak login, workspace UUID
Default dev URLs:
| Service | URL |
|---|---|
| Control Plane API | http://localhost:8088 |
| Keycloak | https://dev-auth.groundfloor.co (or local) |
| LiteLLM | http://localhost:4000 |
| MinIO (files) | http://localhost:9000 |
| Infisical (secrets) | http://localhost:8081 |
Set CONTROLPLANE_URL, WORKSPACE_ID, and obtain a token before running authenticated recipes.
Get a Keycloak access token (dev)
Option A (recommended) — gf CLI login
The gf CLI (in packages/cli) logs in once via the browser and caches a
refreshing session, so you do not have to hand-copy tokens. See
packages/cli/README.md for setup.
gf login # opens the browser, signs in via Keycloak
gf workspaces use <uuid> # pick a default workspace
eval "$(gf env)" # exports GROUNDFLOOR_TOKEN, CONTROLPLANE_URL, GROUNDFLOOR_WORKSPACE_ID
# or grab a fresh token directly:
export TOKEN="$(gf token)"
export CONTROLPLANE_URL="http://localhost:8088"Verify end-to-end: gf whoami and gf coderunner ls.
Deploy a coderunner straight from your project (local folder or a remote repo):
gf deploy # zip + deploy the current folder
gf deploy --git https://github.com/me/fn.git --ref maingf deploy creates the coderunner if needed, uploads the code, waits for the
build, deploys, and prints the deployment URL. See
packages/cli/README.md.
Option B — from Portal / Shell browser session
- Sign in to the Customer Portal or Shell with Keycloak.
- DevTools → Application → copy session or use your app's
getToken()helper. - Export for curl:
export TOKEN="eyJhbG…"
export WORKSPACE_ID="42e69574-d2e2-4ba6-8594-6a71a44d1936"
export CONTROLPLANE_URL="http://localhost:8088"Option C — Keycloak direct grant (dev clients only)
If your Keycloak client allows password grant (dev-only):
curl -s -X POST "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=${CLIENT_ID}" \
-d "username=${USER}" \
-d "password=${PASS}" | jq -r .access_tokenPrefer PKCE browser flow for Shell apps; use direct grant only for scripts.
Shell bootstrap (no auth)
export SHELL_WORKSPACE_ID="${WORKSPACE_ID}"
export SHELL_APP_SLUG="my-app"
curl -s "${CONTROLPLANE_URL}/v1/public/workspaces/${SHELL_WORKSPACE_ID}/apps/by-slug?slug=${SHELL_APP_SLUG}" | jq .Check manifest.remoteUrl and release.build_number.
Verify remote entry is reachable:
REMOTE=$(curl -s "${CONTROLPLANE_URL}/v1/public/workspaces/${SHELL_WORKSPACE_ID}/apps/by-slug?slug=${SHELL_APP_SLUG}" | jq -r .manifest.remoteUrl)
curl -sI "${REMOTE}" | head -1List workspace apps (authenticated)
curl -s "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/apps" \
-H "Authorization: Bearer ${TOKEN}" | jq .Secrets
List keys (no values):
curl -s "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/secrets" \
-H "Authorization: Bearer ${TOKEN}" | jq .Upsert:
curl -s -X PUT "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/secrets/DEMO_KEY" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"value":"demo-secret","description":"Local dev test"}' | jq .Reveal (audited — dev only):
curl -s "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/secrets/DEMO_KEY" \
-H "Authorization: Bearer ${TOKEN}" | jq .LLM Gateway
Mint virtual key (requires workspace admin):
curl -s -X POST "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/llm/virtual-key" \
-H "Authorization: Bearer ${TOKEN}" | jq .Store in secrets:
VKEY=$(curl -s -X POST "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/llm/virtual-key" \
-H "Authorization: Bearer ${TOKEN}" | jq -r .virtual_key)
curl -s -X PUT "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/secrets/LITELLM_API_KEY" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"value\":\"${VKEY}\",\"description\":\"Dev virtual key\"}" | jq .List models:
curl -s "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/llm/models" \
-H "Authorization: Bearer ${TOKEN}" | jq '.models[] | {gf_id, origin, provider}'Chat completion via LiteLLM:
export LITELLM_URL="http://localhost:4000"
export LITELLM_API_KEY="${VKEY}"
curl -s "${LITELLM_URL}/v1/chat/completions" \
-H "Authorization: Bearer ${LITELLM_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gf-chat-default",
"messages": [{"role": "user", "content": "Say hello in one word"}]
}' | jq .Usage rollup:
curl -s "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/llm/usage?since_seconds=3600" \
-H "Authorization: Bearer ${TOKEN}" | jq '{total_cu, total_tokens, row_count: (.rows | length)}'Data Vault
List collections:
curl -s "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/vault/collections" \
-H "Authorization: Bearer ${TOKEN}" | jq .Query with filter and pagination:
curl -s -X POST "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/vault/collections/my_collection/query" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"filter": { "status": "active" },
"sort": ["-created_at"],
"limit": 10,
"offset": 0
}' | jq '{total, has_more, count: (.items | length)}'Create a record:
curl -s -X POST "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/vault/collections/my_collection" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"record": {"name": "Test", "status": "active"}}' | jq .See 04-data-vault.md. If you get 502 with Dataplane auth hints, ensure workspace provisioning completed and DATAPLANE_SERVICE_API_KEY exists in Infisical.
Files
Full upload flow:
# 1. Init
INIT=$(curl -s -X POST "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/files/upload-url" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"test.txt","content_type":"text/plain","size_bytes":12}')
FILE_ID=$(echo "$INIT" | jq -r .file.id)
UPLOAD_URL=$(echo "$INIT" | jq -r .upload_url)
# 2. PUT bytes
echo "hello world" | curl -s -X PUT "${UPLOAD_URL}" \
-H "Content-Type: text/plain" --data-binary @-
# 3. Finalize
curl -s -X POST "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/files/${FILE_ID}/finalize" \
-H "Authorization: Bearer ${TOKEN}" | jq .
# 4. Download URL
curl -s "${CONTROLPLANE_URL}/v1/workspaces/${WORKSPACE_ID}/files/${FILE_ID}/download-url" \
-H "Authorization: Bearer ${TOKEN}" | jq -r .download_urlSee 05-files.md. Requires MinIO — docker compose -f deploy/docker-compose.phase2-deps.yml --profile minio up -d.
Bring up Phase 2 dependencies
docker compose -f deploy/docker-compose.phase2-deps.yml --profile all up -dSee Phase 2 deps runbook for Infisical and LiteLLM bootstrap.
Portal UI shortcuts
- Workspace UUID: Portal URL
/workspaces/{uuid}/… - Shell bootstrap URLs: App detail → Shell bootstrap card (copy buttons)
- OpenAPI:
{CONTROLPLANE_URL}/docs