Outcome
Save a reviewed Consent project configuration, with conditional admin control over allowed surfaces and explicit collection, risk and cache effects.
Action: app_topolo_consent.projects.config.update.
PUT /api/projects/{projectKey}/config
Before you start
Verify original production identity/organization, canonical Consent workspace grant and app_topolo_consent.workspace:write; discover the real reachable non-archived project/current complete configuration. Bind actual workspace separately from project key or organization id.
Ordinary saves require read_write-or-higher workspace access. A change to allowed domain/app membership additionally requires admin. Approve the full intended configuration, allowed collection surfaces, policy content, storage/cache rewrite and possible compliance-risk notification effects before a real mutation.
Preserve current configuration/collection membership and concurrent-owner state for reconciliation. These fixtures are fictional and UNRUN; no configuration, policy, risk record, domain/app, privacy event, model/provider or notification operation was executed. English-source labels in the full response retain the explicitly assumed fixture translator, not a deployed translation claim.
Request and field semantics
Discover current schema, validate reviewed input and inspect its resource-bound plan. Confirmation must cover full normalized collection/policy effects. Fields with broad JSON schemas still follow the source normalization rules below. Fictional ids/domains authorize no execution.
Input accepts projectKey plus environment,mode,domains,apps,purposes,vendors,copy,compliance,cacheTtlSeconds,staleWhileRevalidateSeconds. The last two fields are advertised numeric inputs but the selected normalizer never reads them: current config values are retained. Do not expect TTL changes from a schema-valid save containing those fields.
Omitted or unsupported-shape collection fields can fall back to the resolved current config; array values are normalized/filtered and capped100. domains also accepts newline-separated text. apps keeps only supported ios/android/flutter platform plus nonblank identifier; purposes/vendors normalize ids/fields and drop invalid entries. An empty array can clear the corresponding normalized collection; do not send [] as an unknown placeholder. Broad {} input schemas do not imply that every JSON value is meaningfully applied.
Purpose categories default to support when unsupported, essential becomes required, and defaultDecision is normalized by source. Copy merges six known nonblank string fields with current fallbacks, so empty strings do not simply clear displayed text. Compliance normalizes its defined fields/lists with fallback values; this is configuration handling, not a legal correctness or policy approval assessment.
The admin-field check compares trimmed/lowercased domain and platform/identifier membership sets before and after normalization. A real membership change requires admin; mere array reorder/duplicate representation does not necessarily require it, and this comparison is not comprehensive DNS/site/app ownership validation. A read_write caller can pass the route gate and still receive403 workspace_access_insufficient for a body that changes allowed surfaces.
The handler updates project environment/region_mode/config_json/updated_at, then replaceProjectCollections deletes all existing domain/app, purpose and vendor rows for the project and inserts the normalized full next sets in separate writes. Even a copy-only save rewrites these collections and can persist resolved fallback defaults. There is no surrounding transaction, expected revision or CAS shown; partial failure/concurrent work can leave mixed state.
Stored config_json is rebuilt from sdkKey,copy,compliance,cacheTtlSeconds,staleWhileRevalidateSeconds; other old custom keys are not generically preserved. Environment is project metadata, not platform environment selection; this save does not automatically rotate the retained SDK key. Normalizer/response configuration and later cache/default fallbacks can differ from a fresh subsequent DB projection, especially after empty collection writes.
After collection writes, compliance-risk reconciliation starts: five missing policy/rights/retention/checklist facts can open/update a project_configuration_incomplete record and its detection version; complete inputs can resolve open records. New/changed persisted detections can invoke notification handling. This asynchronous work starts regardless of execution-context availability; ctx.waitUntil retains it when available, and response provides no risk/delivery completion receipt. It is not a model-generated or legally validated risk assessment.
Config cache publication starts independently and attempts SDK mapping, with ctx.waitUntil when available and a250ms local wait. Response is {config,cacheStatus,source:d1}; synced means cache promise resolved before deadline, queued means timeout/rejection—not a durable job receipt. Missing KV or caught mapping failure can still resolve without proven persistence; a200 config body is not global CDN/browser adoption, risk completion or notification delivery.
Repeating a save changes timestamps, rewrites collections and may alter risk/cache state even when apparent values match. No idempotency key is accepted. Failed response is not proof that no rows or notifications changed; preserve uncertain partial effects and avoid parallel duplicate saves.
Complete worked responses
Update copy while advertised TTL fields remain unchanged
Complete source-derived synthetic/unrun HTTP200. Assumes original read_write/write grant, reachable active fixture and complete current config from the prior creation fixture, unchanged empty domain/app sets and successfully normalized/re-written collections. English source strings remain fixture values; cache promise resolves before250ms. Input TTL fields are not read by this source normalizer, so response retains300/86400. No actual save/risk/cache/translation operation occurred.
Input:
{
"projectKey": "fixture-project",
"copy": {
"title": "Updated fixture choices"
},
"cacheTtlSeconds": 3600,
"staleWhileRevalidateSeconds": 0
}
Response:
{
"config": {
"tenantId": "org_record_fixture",
"authOrgId": "org_fixture",
"projectId": "cp_00000000-0000-4000-8000-000000000001",
"workspaceId": "ws_consent_fixture",
"projectKey": "fixture-project",
"sdkKey": "pk_live_tc_17pdn1f11b86se07rgyil",
"mode": "global",
"updatedAt": "2026-10-20T08:01:00.000Z",
"cacheTtlSeconds": 300,
"staleWhileRevalidateSeconds": 86400,
"purposes": [
{
"id": "essential",
"label": "Essential operations",
"category": "essential",
"required": true,
"defaultDecision": "granted"
},
{
"id": "advertising",
"label": "Advertising and sale/share",
"category": "advertising",
"required": false,
"defaultDecision": "unknown"
},
{
"id": "ai_training",
"label": "AI training and improvement",
"category": "ai_training",
"required": false,
"defaultDecision": "unknown"
},
{
"id": "analytics",
"label": "Analytics",
"category": "analytics",
"required": false,
"defaultDecision": "unknown"
},
{
"id": "personalization",
"label": "Personalization",
"category": "personalization",
"required": false,
"defaultDecision": "unknown"
},
{
"id": "support",
"label": "Support workflows",
"category": "support",
"required": false,
"defaultDecision": "unknown"
}
],
"vendors": [
{
"id": "google",
"name": "Google",
"purposes": [
"analytics",
"advertising"
]
},
{
"id": "meta",
"name": "Meta",
"purposes": [
"advertising"
]
},
{
"id": "topolo",
"name": "Topolo services",
"purposes": [
"essential",
"support",
"ai_training"
]
},
{
"id": "stripe",
"name": "Stripe",
"purposes": [
"essential"
]
}
],
"domains": [],
"apps": [],
"copy": {
"title": "Updated fixture choices",
"summary": "Choose optional data uses by purpose. Essential operations stay on, and you can change or withdraw optional choices later.",
"acceptAll": "Allow optional uses",
"rejectAll": "Reject non-essential",
"manage": "Manage choices",
"gpcApplied": "Global Privacy Control is enabled in this browser, so sale/share and restricted optional uses were opted out automatically."
}
},
"cacheStatus": "synced",
"source": "d1"
}
Check applied title and retained TTL values. Synced is not risk reconciliation, persisted SDK mapping, global serving or delivered-notification proof.
Read-write caller cannot add an allowed site
Complete source-derived synthetic/unrun HTTP403. Assumes original worker read_write/write checks pass, active reachable fixture has empty domains/apps, and this body adds a real membership element. The conditional admin check fails before DB collection/config writes. The fictional domain was not fetched or added.
Input:
{
"projectKey": "fixture-project",
"domains": [
"site.fixture.invalid"
]
}
Response:
{
"error": "This workspace requires admin access",
"code": "workspace_access_insufficient"
}
Preserve intended target and refusal. Obtain the appropriate original authorized admin decision/grant rather than dropping policy fields or broadening principal to bypass the check.
Archived project refused
Complete synthetic/unrun HTTP409. Assumes prior original authority/storage/project reachability succeeds and stored target is archived. No status change or save occurred.
Input:
{
"projectKey": "fixture-project",
"copy": {
"title": "Updated fixture choices"
}
}
Response:
{
"error": "project_archived"
}
Preserve refusal; project activation is a separate approved admin operation, not an automatic config-save retry.
Verify and recover
Compare actual returned config.projectKey/projectId/workspaceId and intended fields against original current scope. Retain actual sourceRevision/request evidence and distinguish advertised-but-ignored TTL fields from applied values. A normalized response is not proof that every collection/cache/risk change persisted atomically.
Reconcile actual project and normalized collection state with the current owner and supported validated readers before another mutation. Later config reads may use cache/default fallbacks; preserve actual validation/permission/dependency refusals rather than manufacturing a fresh DB or full-coverage claim.
Inspect cache and compliance-risk/notification outcomes independently where the existing supported owner workflow supplies them. cacheStatus refers only to its cache promise, not the risk task. Do not send a sample consent event, scan a domain or invoke a model merely to verify configuration.
After any uncertain/partial write, compare actual current state and intervening owner work with the retained preimage before considering compensation. Do not repeat a broad save to force a timestamp, default, notification or cache result.
Only with current reconciliation and authorization, submit a separately reviewed supported previous configuration through the same action and appropriate admin grant for surface changes. This rewrites state again; it does not restore original timestamps, deleted row identities, risk detection history or delivered notifications, and ignored TTL inputs cannot restore arbitrary old TTLs. Preserve concurrent changes and unknown outcomes; do not use direct SQL/KV edits or organization erasure as rollback.
Refusals
403 / workspace_access_insufficient: Body changes allowed domains/apps but current verified workspace grant is insufficient. Preserve exact requested surfaces and seek proper current authorization; do not bypass by changing identity or silently omitting intended restrictions.
403 / organization_context_required: Reconcile original intended organization context.
404 / project_not_found: Reconcile real project key in original organization AND verified workspace.
409 / project_archived: Preserve archive; restoration requires separate explicit admin authority.
503 / database_unavailable: Control DB absent. Later storage/cache failures require current partial-state reconciliation and actual runtime evidence before another write.
Outer Auth/workspace/app-permission checks precede the handler; retain actual shared carrier and request evidence. Broad valid inputs can be normalized or ignored rather than returning a fabricated validation error. No live save or risk/delivery result was captured here.
Change log
- 2026-10-21: Added normalized-field/ignored-TTL semantics, conditional admin surface changes, whole-collection rewrite and asynchronous risk/cache effects, partial-write recovery and three complete illustrative unrun responses.