APS Activation Runbook — taking the Model Derivative pipeline live (roadmap R1.1)¶
Status: DORMANT → ACTIVE runbook. The APS derivative pipeline is fully built and tested against fixtures, but no live Autodesk Platform Services credentials exist in any deployment, so every Autodesk-native upload is rejected at the door today.
Grounding: this runbook was written from the actual code —
apps/server/src/modules/integrations/aps/client.ts, .../aps/derivative-job.ts,
apps/server/src/config.ts, apps/server/src/modules/models/routes.ts,
apps/server/src/jobs/reconcile-conversions.ts. Every step below cites what the code actually does.
1. What exists already (do not rebuild)¶
| Capability | Where |
|---|---|
| 2-legged APS auth, OSS bucket create, signed-S3 upload (streamed), translate job, manifest poll, derivative download (file-streamed) | apps/server/src/modules/integrations/aps/client.ts (ApsClient) |
| Full state machine: upload → translate → poll → download IFC → re-stage → local conversion | apps/server/src/modules/integrations/aps/derivative-job.ts (runApsDerivative) |
| Queue + worker registration | derivative-job.ts:125-133 (enqueueApsDerivative / registerApsDerivativeWorker), wired in apps/server/src/jobs/registry.ts:27,66 |
Upload gate + sourceType: 'aps' routing |
apps/server/src/modules/models/routes.ts:90-97 (rejection when unconfigured) and routes.ts:130,182-188 (branch that stages the source and enqueues) |
| Redelivery guard (at-least-once safety) | derivative-job.ts:50-66 |
| Bounded retries | derivative-job.ts:126 — retryLimit: 2, retryDelay: 60, meaningful because runApsDerivative rethrows (derivative-job.ts:103-111) |
| Realtime progress | setApsModel emits model:status with apsDerivativeStatus (derivative-job.ts:24-34) — the existing web UI needs zero changes |
The code comment on client.ts:8-12 says it plainly: "Live translation is the only credential-blocked
step." Everything else is exercised by tests.
2. Prerequisites¶
- [ ] A deployment whose
PUBLIC_URLand database/storage are production-shaped (the pipeline streams multi-GB files through temp dirs; seederivative-job.ts:68-70). - [ ] Operator access to the secrets store for the deployment env.
- [ ] The worker process running — the derivative worker registers via
registerApsDerivativeWorker()injobs/registry.ts:66; it does not run inside the API process.
3. Step 1 — Create the APS app and credentials¶
- Sign in at https://autodesk.com/apis (APS developer portal) with the company Autodesk account.
- Create an app of type Server-to-Server (this pipeline uses only the client-credentials /
2-legged flow —
client.ts:68-81,grant_type=client_credentials). - Request/enable these services on the app:
- Model Derivative (translate + manifest)
- Object Storage Service / Data Management (bucket create, signed S3 upload/download)
- Webhooks only if ACC activation follows (see
docs/runbooks/acc-procore-connect.md) — not needed for R1.1 alone. - Record the Client ID and Client Secret. The default token scopes requested by
getTwoLeggedTokenare"data:read data:write data:create bucket:create bucket:read"(client.ts:69). If your APS account requires scope allow-listing per app, allow exactly those. - Decide the OSS bucket key. Default if unset:
bimzone-derivatives(derivative-job.ts:48,config.APS_OSS_BUCKET ?? "bimzone-derivatives"). Bucket policy created istransient(client.ts:88) — objects are deleted by Autodesk after ~24h, which is intended: the derived IFC is downloaded and stored in BimZone's own storage; APS OSS holds nothing durable.
Note: do NOT reuse the
AUTODESK_CLIENT_ID/AUTODESK_CLIENT_SECRETsign-in OAuth pair here. Those are a separate 3-legged sign-in app (config.ts:105-110); the data pipeline uses its own server-to-server app.
4. Step 2 — Set environment variables¶
Exact names from apps/server/src/config.ts:127-131:
| Env var | Required | Notes |
|---|---|---|
APS_CLIENT_ID |
yes for activation | optional in schema; its absence is what keeps the pipeline dormant — uploads of .rvt/.nwd/.nwc/.dwg are rejected with a 400 naming it (models/routes.ts:95-97) |
APS_CLIENT_SECRET |
yes for activation | ApsClient throws "APS_CLIENT_ID and APS_CLIENT_SECRET are required" without both (client.ts:60) |
APS_BASE_URL |
no | defaults to https://developer.api.autodesk.com (config.ts:130) |
APS_OSS_BUCKET |
no | defaults to bimzone-derivatives (derivative-job.ts:48) |
Set them on BOTH processes that read config: API (upload route checks config.APS_CLIENT_ID) and
worker (constructs ApsClient). Config is parsed and validated once at import (config.ts:189-198);
a change means restart, not hot-reload.
Roll out: set vars in staging first, restart API + worker, confirm boot succeeds (an invalid value
fails fast at first use, since the vars themselves are .optional()).
5. Step 3 — Webhooks/callback URLs¶
None required for R1.1. The pipeline polls the Model Derivative manifest
(pollManifestUntilDone, client.ts:138-155: 5 s interval × 120 attempts ≈ 10 min ceiling). There is
no callback URL to register for translation events. Webhook endpoints exist only for the ACC/Procore
adapters — see the sibling runbook.
5b. Executed 2026-09-05 — client-level run against the live APS tenant¶
apps/server/scripts/aps-runbook.ts drives the exact client the worker uses (token → bucket → signed
S3 upload → translate ifc+svf2 → manifest poll → IFC derivative download) against a local file and
writes an evidence record. Run with the deployment's APS_* env plus the usual DATABASE_URL/
JWT_SECRET/APP_SECRET:
npx tsx scripts/aps-runbook.ts <file.rvt> <evidence.json>
Evidence: docs/runbooks/evidence/aps-2026-09-05.json — rac_basic_sample_project.rvt (17.1 MB, the
public Autodesk sample): token 0.5 s · bucket 1.3 s · upload done at 57.6 s · translate accepted at 59.3 s ·
manifest success (svf2 + thumbnail + ifc) at 157.3 s · derived IFC 19.7 MB downloaded at 159.4 s.
Defect the first run surfaced (fixed in the same commit): the streamed S3 presigned PUT went out with
Transfer-Encoding: chunked and no length, and S3 answered 501. ApsClient.uploadToOss now requires a
contentLength for stream bodies and sends Content-Length; derivative-job.ts passes the staged file's
size. Unit-tested in tests/aps-client.test.ts. Sources over 5 GB still need the multi-part
(?parts=N) variant — not built; the current 5 GB/part cap is noted in the client.
Still open from §6: steps 2, 3, 5–8 need the whole stack (API + worker + storage) on a staging deployment; the 30-day production sync (P2-12) is a calendar item once one account is linked.
6. Step 4 — Staging verification checklist¶
Run end-to-end in staging before production:
- [ ] Upload gate flips.
POST /api/projects/:projectId/modelswith a small.rvtreturns 201 (previously 400 "…requires Autodesk Platform Services"). Row showssourceType: 'aps',status: processing,apsDerivativeStatus: queued(models/routes.ts:182-188). - [ ] Job picked up. Worker log shows the pg-boss job from queue
QUEUES.apsDerivative(singletonKey = modelId). - [ ] State transitions observable. Via websocket
model:statusor the models list, watch:queued → uploading → translating → converting(derivative-job.ts:72-96). ConfirmapsUrnis populated on the row after upload (base64url of the OSS objectId,client.ts:63-66). - [ ] IFC handoff. After the manifest reports success,
findDerivativeUrn(manifest, "ifc")resolves and the IFC downloads to disk (downloadDerivativeToFile,client.ts:172-185). The derived IFC is staged understorageKey.modelIfc(...)andifcKeyis rewritten; the original RVT blob is best-effort deleted (derivative-job.ts:91-101). - [ ] Conversion completes.
enqueueConversionruns the existing IFC→frag pipeline; model endsstatus: ready, viewer renders. - [ ] Auto-clash fires. On
ready,scheduleAutoClashruns (modules/models/conversion.ts:175) and enqueues clash-run jobs for eligible auto-clash tests bound to the version group (lib/auto-clash.ts:21-55). - [ ] Retry path. Simulate a transient failure (e.g. revoke the secret mid-flight once): the job
should retry up to 2 more times at 60 s spacing, and the redelivery guard must resume rather
than restart once past
translating(derivative-job.ts:63-66). - [ ] Failure path. Upload a corrupt file that fails translation: row lands
status: failed,apsDerivativeStatus: failedwith the manifest error text surfaced viadescribeManifestError(client.ts:207-213); UI offers retry.
7. Rollback & failure modes¶
- Kill switch: unset
APS_CLIENT_ID(+ restart). New Autodesk-file uploads immediately 400 (models/routes.ts:95-97). In-flight jobs will fail atnew ApsClient()and exhaust their bounded retries intofailed— acceptable and visible. - Vendor incident: there is no separate flag for APS alone; the global outbound-integration kill
switch
INTEGRATIONS_ENABLED(config.ts:126) governs the integration sync workers, not the model pipeline. Treat credential removal as the APS off-switch. - Stuck derivatives — the known monitoring gap. The startup reconcile sweep explicitly EXCLUDES
APS models:
notInArray(models.sourceType, ["pointcloud", "aps"])(jobs/reconcile-conversions.ts:131). The comment (:126-130) explains why: re-enqueueing an APS model through the IFC path would convert the binary RVT source and flap failed→ready. "Their quiet-period recovery belongs to the APS job" — i.e. recovery rests entirely on pg-boss retries (retryLimit: 2). Gap: after 3 attempts, nothing sweeps an APS model stranded inprocessing/translatingby a crash or a >10-minute translation. Until a dedicated APS sweep exists, monitor manually: - Query: models where
sourceType='aps'AND status non-terminal ANDupdated_atolder than 15 minutes (same liveness signal the IFC sweep uses,reconcile-conversions.ts:33-43). - Remediation per state: stuck at
converting/done→ safe to re-enqueue conversion directly (the guard itself proves idempotence,derivative-job.ts:57-66); stuck earlier → requeue the derivative job (singletonKey dedupes) or fail the row so the user sees the retry affordance. - Data-loss hazard (already guarded, do not weaken): after
converting, the original RVT/NWD is deleted; a naive retry would destroy the model. This is precisely what the redelivery guard protects (derivative-job.ts:50-62).
8. Go-live checklist¶
- [ ] Production secrets set on API + worker; both restarted; boot green.
- [ ] One real
.rvtuploaded in production by a test project → reachesready, viewer renders, auto-clash scheduled. - [ ] One deliberate failure exercised (bad file) → clean
failedstate, error message readable. - [ ] Stuck-model monitoring query/alert scheduled (Section 7 gap) — or a ticket filed for the dedicated APS reconcile sweep before volume arrives.
- [ ] APS portal: spend limits / usage alerts configured (translation is metered per GB).
- [ ] Client secret stored in the secrets manager only; never committed (gitleaks runs in CI,
.github/workflows/security.yml). - [ ] This runbook updated if any cited line moves —
scripts/check-control-matrix.mjswill catch drift only for paths cited in the SOC 2 matrix, not here.