Outcome
Save the complete entry-experience draft using its exact current revision, keeping saved, published and running experience distinct.
PUT /api/developer-console/apps/{appId}/experience
Before you start
Use the original organization and published Developers apps:write permission. Discover the accessible canonical app and Developers workspace and bind the workspace separately at the currently required write access. Read the exact saved authoring state/revision before any intended change.
The handler resolves a non-suspended/non-revoked context, requires the selected app’s workspace match and actor.canEdit, then denies a read-only context. This entry-experience handler does not itself add a completed-onboarding check; do not invent that requirement or infer that other actions have the same policy. Its current native action/resource authorization still applies.
Ordinary same-organization workspace selection is Auth-verified; the existing privileged cross-organization platform branch uses its seeded profile workspace without honoring a requested header. Neither branch removes the app-workspace and actor checks. Shared context can insert a local mirror/initial profile before later refusal, without minting an Auth workspace.
Review the complete intended draft before authorizing this mutation. Native input is exactly appId,expectedRevision,draft with login and landing objects. Validate and plan the actual discovered payload/resource context and obtain authority for that concrete write; a confirmation flag does not supply human approval.
All examples are complete source-derived synthetic UNRUN carriers with fictional ids and saved-state premises. No app/private authoring, Auth, catalog publication, login/signup, provider/key or business mutation was performed; no application import/test, source edit or runtime probe.
Request and effects
Action: app_topolo_developers.apps.experience.update. This is a confirmed mutation.
The complete successful HTTP200 envelope is {success:true,message,data:{appId,kind:"entryExperience",state}}. It does not add canWrite or a publication receipt to the save response. This is a saved-draft result, not a live login/signup or app deployment result.
The body must contain a safe integer expectedRevision exactly equal to the current saved state.revision. A mismatch/missing value produces rawHTTP409 Read the current revision before saving or publishing. The native schema additionally governs admitted input types/fields; source aliases/extra properties are not permission to bypass it.
A saved publication with status publishing or failed blocks editing even at the matching revision, returning rawHTTP409 Resume or finish the current publication before editing. This PUT does not itself reconcile a catalog receipt or reset/resume an unresolved attempt. Preserve the exact original attempt and outcomes.
draft must be a nonarray object and match the strict experienceDraftInput with both login and landing objects, then the locked published app-manifest validator. Root keys other than login/landing and unknown surface keys are refused. Missing or malformed draft produces422; validation checks are separate from caller authorization.
login may contain layout(split/centered/immersive/minimal/magazine), background_type(gradient/image/video/animated), two background_gradient color fields, background_image_url,background_video_url and background_animation. landing permits its two gradient fields plus show_topolo_one,pricing_enabled,is_enabled booleans. Empty login/landing objects are allowed by these source validators. This schema is app presentation data, not an arbitrary Auth/security/permission configuration channel.
Use lower-case six-digit #rrggbb colors to match the current published JSON schema pattern. The source Zod/package validators also accept uppercase hex, but the published pattern lacks the case-insensitive flag; do not send a payload the current native schema rejects. Asset URL values must be HTTPS without URL username/password, beyond the generic URI format in the published schema. Source string/asset validation is not a media fetch, reachability or sanitization certificate.
The helper replaces state.draft with the complete supplied draft and increments revision by1; it does not deep-merge omitted presentation keys from the prior draft. It spreads the prior state, retaining any published snapshot, publication metadata and migration archive unless changed by a separately authorized operation. Saving a new draft does not invoke the catalog publisher or commercial/Auth broker.
The single SQL update guards the current revision, retained publication attempt identity and full prior authoring JSON before writing the next state into app metadata and updating the control row timestamp. If it cannot return an updated row, the route returns409 App authoring changed concurrently. A fresh read is required; do not reconstruct an older expected state or blindly force a stale draft.
No cross-service operation or automatic inverse is part of this PUT. The saved revision is not a Git SHA or running UI version. For a deliberate later restoration, read current revision/state and review a new complete draft change with concrete authority; publication/recovery has its own current contract.
Worked requests and responses
Save the first complete draft
Complete source-derived synthetic UNRUN HTTP200, assuming absent authoring maps to revision0/draft:null/published:null/publication:null, all authorization/validation succeeds and the guarded update wins. No login/signup or catalog operation was run.
Input:
{
"appId": "app_docs_fixture",
"expectedRevision": 0,
"draft": {
"login": {
"layout": "centered"
},
"landing": {
"is_enabled": true,
"show_topolo_one": false,
"pricing_enabled": false
}
}
}
Response:
{
"success": true,
"message": "OK",
"data": {
"appId": "app_docs_fixture",
"kind": "entryExperience",
"state": {
"revision": 1,
"draft": {
"login": {
"layout": "centered"
},
"landing": {
"is_enabled": true,
"show_topolo_one": false,
"pricing_enabled": false
}
},
"published": null,
"publication": null
}
}
}
Read back exact revision1 and full draft. This saved-state result is not an active experience or permission grant.
Replace a previously saved unpublished draft
Complete synthetic UNRUN HTTP200 assuming current state is exactly the first example’s revision1, no publication is pending and the guarded update wins. New landing{} replaces the prior landing flags; it is not a patch retaining them.
Input:
{
"appId": "app_docs_fixture",
"expectedRevision": 1,
"draft": {
"login": {
"layout": "minimal"
},
"landing": {}
}
}
Response:
{
"success": true,
"message": "OK",
"data": {
"appId": "app_docs_fixture",
"kind": "entryExperience",
"state": {
"revision": 2,
"draft": {
"login": {
"layout": "minimal"
},
"landing": {}
},
"published": null,
"publication": null
}
}
}
Verify revision2 and the exact replacement draft rather than assuming omitted flags survived or that publication occurred.
A stale revision is refused
Complete synthetic UNRUN rawHTTP409 when the actual saved state is revision1 but this schema-valid request still expects0. The selected authoring write does not run; earlier shared context effects are separate.
Input:
{
"appId": "app_docs_fixture",
"expectedRevision": 0,
"draft": {
"login": {
"layout": "centered"
},
"landing": {
"is_enabled": true,
"show_topolo_one": false,
"pricing_enabled": false
}
}
}
Response:
{
"success": false,
"error": {
"message": "Read the current revision before saving or publishing"
}
}
Read current exact state and reassess the intended change; no blind retry or revision guess.
An unresolved publication blocks editing
Complete synthetic UNRUN rawHTTP409 with matching revision1 and an existing publication.status publishing or failed. This PUT does not resume it, invent a receipt or reset the attempt.
Input:
{
"appId": "app_docs_fixture",
"expectedRevision": 1,
"draft": {
"login": {
"layout": "minimal"
},
"landing": {}
}
}
Response:
{
"success": false,
"error": {
"message": "Resume or finish the current publication before editing"
}
}
Preserve exact original attempt/outcomes and consult the current supported recovery/read contract before any separately authorized mutation.
Verify and recover
-
Read apps.experience.get in the original app/workspace and compare the exact saved revision and full intended draft, retaining the separate published snapshot/publication/migration state. A GET can return a virtual receipt reconciliation, so distinguish it from raw persisted control state/outcomes.
-
Verify the actual request passed both the current native schema and source semantic constraints; a local schema pass is not caller authority, URL/media availability or runtime correctness. Preserve actual status/error/request id.
-
After a409 or lost response, read exact current revision/attempt/outcomes and reassess with the legitimate owner. Do not reset an unresolved publication, overwrite another editor or infer that a failed request rolled back all shared initialization.
-
Any intentional restoration/publication is a new reviewed, authorized current-state operation; no Auth/copy/media/provider/login probe or automatic inverse is part of draft-save verification.
Change log
- 2026-10-21: Added exact revision/attempt/full-state concurrency behavior, complete-draft replacement, presentation/URL validation and unresolved-publication refusal, with full synthetic unrun success and409 examples. No live authoring/publication/Auth operation was performed.