# 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:

```bash
# dev-control canonical entrypoint is the Tailscale Funnel API host.
# Internal-LAN operators may also use https://api.retired-dev-control.example.invalid
# (the pre-Funnel form). See doc/operations/Dev_Control_Funnel_Canonicalization_v1.md.
export API_BASE_URL="https://gpuaas-dev-api.tailfe39f5.ts.net"
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:

```bash
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:

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

## Step 2: Create a Project Service Account
Create the operator identity for the app.

```bash
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:

```bash
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.

```bash
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:

```bash
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.

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

```bash
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.

```bash
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.

```bash
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.

```bash
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:

```bash
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:

```bash
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.

```bash
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:

```bash
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.

```bash
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:]')"
```

```bash
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:

```bash
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:

```bash
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:

```bash
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.

```bash
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.

## Related Docs
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`
