Groundfloor Docs

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:

ServiceURL
Control Plane APIhttp://localhost:8088
Keycloakhttps://dev-auth.groundfloor.co (or local)
LiteLLMhttp://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)

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 main

gf 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

  1. Sign in to the Customer Portal or Shell with Keycloak.
  2. DevTools → Application → copy session or use your app's getToken() helper.
  3. 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_token

Prefer 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 -1

List 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_url

See 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 -d

See 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

On this page