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:
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:
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:
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.
- Project-specific registry or private artifact tenancy is not yet productized.
- Runtime-specific operator implementations are still reference-level, not productized.
platform_managedis modeled but should not be used as the default assumption.- App-runtime pricing/rating beyond usage-record attribution is still baseline-only.
Anti-Patterns¶
- Do not automate app lifecycle with user tokens.
- Do not bypass project entitlements.
- Do not depend on direct database access.
- Do not assume one environment or one registry host forever.
- Do not create a second billing store.
- Do not create app-specific authz bypasses for internal operators.
Related Docs¶
doc/architecture/Build_an_App_for_GPUaaS_v1.mddoc/architecture/App_Runtime_Metering_v1.mddoc/architecture/Node_Operations_and_Agent_Lifecycle_v1.md