Skip to content

App Platform Quickstart v1

As of: March 9, 2026

Purpose

This is the shortest practical path for a developer or agent to build and operate an app on GPUaaS using the current platform contracts.

Use this when you want to answer: 1. how do I authenticate, 2. how do I enable my app for a project, 3. how do I create and manage an app instance, 4. how do I query billing and lifecycle state, 5. what is missing today.

Preconditions

You need: 1. a tenant and project, 2. a published app catalog entry and version, 3. a user with permission to manage service accounts and project app entitlements, 4. the API base URL for your environment, 5. a service account for automation.

Canonical references: 1. doc/architecture/Build_an_App_for_GPUaaS_v1.md 2. doc/architecture/App_Control_Plane_v1.md 3. doc/architecture/Service_Account_Model.md 4. doc/architecture/App_Runtime_Billing_Model_v1.md 5. doc/api/openapi.draft.yaml 6. doc/architecture/App_Platform_OCI_Registry_Baseline_v1.md 7. doc/architecture/External_App_Team_Integration_Guide_v1.md 8. doc/architecture/Example_App_Developer_Reference_Workflow_v1.md 9. doc/architecture/App_UI_Extension_Model_v1.md 10. doc/architecture/App_Developer_Starter_Pack_v1.md 11. doc/architecture/App_Manifest_Registration_Guide_v1.md

Variables

Use these shell variables in examples:

export API_BASE_URL="https://api.100-90-157-34.sslip.io"
export PROJECT_ID="<project-uuid>"
export APP_SLUG="<app-slug>"
export APP_VERSION="<version>"
export SA_NAME="app-operator"
export SA_SLUG="app-operator"
export REQUEST_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
export IDEMPOTENCY_KEY="$(uuidgen | tr '[:upper:]' '[:lower:]')"

Step 1: Sign In as a Human Admin

Use a human login only for setup and administration.

Typical local/dev token flow:

curl -s -X POST "$API_BASE_URL/realms/gpuaas/protocol/openid-connect/token"

For browser-driven environments, use the normal OIDC login flow and capture a bearer token from the session if you are testing manually.

Store the human token:

export HUMAN_TOKEN="<human-bearer-token>"

Step 2: Create a Project Service Account

Create the operator identity for the app.

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/service-accounts" \
  -H "Authorization: Bearer $HUMAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $REQUEST_ID" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "name": "'"$SA_NAME"'",
    "slug": "'"$SA_SLUG"'",
    "description": "Operator for app lifecycle automation"
  }'

Expected result: 1. service account created, 2. initial credential material returned by the API, 3. audit log written for the create action.

Record:

export SERVICE_ACCOUNT_ID="<service-account-id>"
export SERVICE_ACCOUNT_KEY_ID="<key-id>"
export SERVICE_ACCOUNT_PRIVATE_KEY_PEM="$(cat /path/to/exported-private-key.pem)"

Step 3: Mint a Short-Lived Service Account Token

Use the service account to obtain a short-lived access token.

curl -s -X POST "$API_BASE_URL/api/v1/auth/service-account/token" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "service_account_id": "'"$SERVICE_ACCOUNT_ID"'",
    "key_id": "'"$SERVICE_ACCOUNT_KEY_ID"'",
    "private_key_pem": "'"$SERVICE_ACCOUNT_PRIVATE_KEY_PEM"'"
  }'

Store the returned token:

export SA_TOKEN="<service-account-bearer-token>"

Rules: 1. do not reuse human tokens for automation, 2. do not persist long-lived bearer tokens in app config, 3. rotate the service-account key when needed instead of extending token lifetime.

Step 4: Inspect the Catalog

List available apps and versions.

curl -s "$API_BASE_URL/api/v1/apps/catalog" \
  -H "Authorization: Bearer $HUMAN_TOKEN" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')"
curl -s "$API_BASE_URL/api/v1/apps/catalog/$APP_SLUG/versions" \
  -H "Authorization: Bearer $HUMAN_TOKEN" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')"

Important UI/API distinction: 1. the platform UI catalog shows only apps enabled for the active project, 2. GET /api/v1/apps/catalog remains the raw catalog API and is not currently entitlement-filtered by itself.

Step 4a: Inspect the Registry Baseline

Use the registry-info endpoint to discover the platform-owned OCI registry contract for the current environment.

curl -s "$API_BASE_URL/api/v1/apps/registry" \
  -H "Authorization: Bearer $HUMAN_TOKEN" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')"

The control-plane truth should give you: 1. registry host, 2. namespace ownership mode, 3. digest-only deployment expectation, 4. whether project private repositories are enabled in this environment.

Step 5: Enable the App for the Project

The app must be entitled before an operator can create instances.

curl -s -X PUT "$API_BASE_URL/api/v1/projects/$PROJECT_ID/apps/entitlements/$APP_SLUG" \
  -H "Authorization: Bearer $HUMAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "enabled": true,
    "policy_overrides": {
      "allowed_operating_modes": ["tenant_dedicated"],
      "allowed_control_plane_scopes": ["project"]
    }
  }'

This is the recommended starting shape: 1. tenant_dedicated 2. project control-plane scope

After entitlement is enabled, the app should appear in the catalog UI for that active project.

Step 6: Create the App Instance

Create the project-owned app instance using the service-account token.

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-instances" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "X-Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "app_slug": "'"$APP_SLUG"'",
    "version": "'"$APP_VERSION"'",
    "display_name": "my-first-app-instance",
    "operating_mode": "tenant_dedicated",
    "control_plane_scope": "project",
    "config": {}
  }'

Read back the response and record: 1. id 2. status 3. operating_mode 4. control_plane_scope 5. runtime_backend 6. tenant_boundary_mode

Important rule: 1. treat the returned values as effective truth, 2. do not assume the request hint is the final topology.

Step 6a: Publish and Register an OCI Artifact

Artifact upload is not proxied through the API.

Use the API to obtain a publish intent first:

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-artifacts/publish-intents" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "X-Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "app_slug": "'"$APP_SLUG"'",
    "app_version": "'"$APP_VERSION"'",
    "artifact_name": "runtime",
    "channel": "dev"
  }'

The publish intent now carries the real delivery details for the current environment: 1. OCI intents return the repository/tag plus Vault-wrapped publish credentials, 2. OCI intents also return signing requirements (signature_required, signature_scheme, signing_key_id, provenance_required), 3. blob intents return a project-scoped upload path and canonical artifact_store://... source URI.

Then push directly to the returned repository using the wrapped credential material from that intent. When signature_required=true, attach the required signature and provenance annotations to the pushed manifest before registration.

After push, register the immutable digest with the control plane:

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-artifacts" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "X-Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "app_slug": "'"$APP_SLUG"'",
    "app_version": "'"$APP_VERSION"'",
    "artifact_name": "runtime",
    "repository": "gpuaas/apps/example/'"$APP_SLUG"'",
    "digest": "sha256:<pushed-digest>",
    "tag": "dev-latest",
    "media_type": "application/vnd.oci.image.manifest.v1+json"
  }'

Important rules: 1. registry push is direct to the registry, not through the API, 2. wrapped Vault credentials are the current publish-credential delivery mechanism, 3. the control plane only trusts immutable digests, 4. later promote/deprecate/retire actions operate on the registered artifact id, not on ad hoc tags.

Step 6b: Publish and Register a Blob Artifact

Use a blob publish intent when the artifact source is project storage instead of OCI.

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-artifacts/publish-intents" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "X-Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "app_slug": "'"$APP_SLUG"'",
    "app_version": "'"$APP_VERSION"'",
    "artifact_name": "weights",
    "artifact_kind": "blob",
    "source_type": "artifact_store",
    "channel": "dev"
  }'

Use the returned upload_path to upload the file into project storage, then register it:

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-artifacts" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "X-Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "app_slug": "'"$APP_SLUG"'",
    "app_version": "'"$APP_VERSION"'",
    "artifact_name": "weights",
    "artifact_kind": "blob",
    "source_type": "artifact_store",
    "source_uri": "artifact_store://projects/'"$PROJECT_ID"'/artifacts/'"$APP_SLUG"'/'"$APP_VERSION"'/weights",
    "digest": "sha256:<uploaded-blob-digest>"
  }'

Blob registration now verifies that the referenced project-storage object exists before the artifact is accepted.

Step 7: Poll the Instance

Use the same service-account token for status reads.

curl -s "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-instances" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')"
curl -s "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-instances/$APP_INSTANCE_ID" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')"

The canonical lifecycle states are: 1. requested 2. deploying 3. running 4. upgrading 5. rolling_back 6. decommissioning 7. decommissioned 8. failed

Step 8: Drive Lifecycle Actions

Use the service account for operator lifecycle actions.

Upgrade:

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-instances/$APP_INSTANCE_ID/upgrade" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "X-Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "target_version": "'"$APP_VERSION"'"
  }'

Rollback:

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-instances/$APP_INSTANCE_ID/rollback" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "X-Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -d '{
    "target_version": "'"$APP_VERSION"'"
  }'

Decommission:

curl -s -X POST "$API_BASE_URL/api/v1/projects/$PROJECT_ID/app-instances/$APP_INSTANCE_ID/decommission" \
  -H "Authorization: Bearer $SA_TOKEN" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "X-Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')"

Step 9: Query Billing

App-runtime usage is exposed through the same billing surface as allocation usage.

curl -s "$API_BASE_URL/api/v1/billing/usage?app_instance_id=$APP_INSTANCE_ID&usage_source=app_runtime" \
  -H "Authorization: Bearer $HUMAN_TOKEN" \
  -H "X-Request-ID: $(uuidgen | tr '[:upper:]' '[:lower:]')"

Expected billing anchors: 1. org_id 2. project_id 3. app_instance_id 4. usage_source = app_runtime 5. operating_mode 6. control_plane_scope 7. runtime_backend

Step 10: Triage Failures Correctly

If a lifecycle action fails: 1. capture the correlation_id from the API error, 2. use logs and traces to follow the lifecycle, 3. confirm the corresponding apps.instance.* events.

Do not diagnose from DB state first.

Artifact and Registry Reality Today

The artifact path is now usable, but not finished.

What exists: 1. live platform OCI registry on platform_control, 2. Vault-wrapped publish credentials for OCI publish intents, 3. trust-state and promotion enforcement in the control plane, 4. signature/provenance verification against real registry manifests, 5. project-storage-backed blob upload intents, 6. registration and verification checks against real registry manifests and real project-storage objects.

What is still missing before broad app-team rollout: 1. workload-specific secret injection beyond artifact pull, 2. productized runtime adapters that consume these artifacts end to end, 3. tenant/project private artifact tenancy beyond the current baseline.

What Is Missing Today

These are the main gaps exposed by the quickstart.

  1. Project-specific registry or private artifact tenancy is not yet productized.
  2. Runtime-specific operator implementations are still reference-level, not productized.
  3. platform_managed is modeled but should not be used as the default assumption.
  4. App-runtime pricing/rating beyond usage-record attribution is still baseline-only.

Anti-Patterns

  1. Do not automate app lifecycle with user tokens.
  2. Do not bypass project entitlements.
  3. Do not depend on direct database access.
  4. Do not assume one environment or one registry host forever.
  5. Do not create a second billing store.
  6. Do not create app-specific authz bypasses for internal operators.
  1. doc/architecture/Build_an_App_for_GPUaaS_v1.md
  2. doc/architecture/App_Runtime_Metering_v1.md
  3. doc/architecture/Node_Operations_and_Agent_Lifecycle_v1.md