FabricFabricAirlift
Getting started

Local lifecycle cookbook

A runnable walkthrough of the governed migration lifecycle on the SDK's in-memory runtime — configured authorization, platform bootstrap, planning, conversion, residue, evidence, certification, and offline verification.

This walkthrough is a single runnable file shipped with the SDK — see the full listing. It drives the whole governed lifecycle through createAirliftRuntime() composed with createAirliftConfiguredAuthorization() — the composition intended for unit tests, examples, and local demos — and it is executable documentation: the repository's test suite runs it on every change, so every snippet below reflects contracts that actually execute.

The cookbook is versioned against action manifest generation 11 and asserts the pin at startup:

import { AIRLIFT_ACTION_MANIFEST_GENERATION } from '@fabricorg/airlift';

if (AIRLIFT_ACTION_MANIFEST_GENERATION !== 11) {
  throw new Error('This walkthrough is versioned against action manifest generation 11.');
}

To run it, install @fabricorg/airlift, save the full listing as local-lifecycle.mjs, and run it with Node.js 22+:

npm install @fabricorg/airlift
node local-lifecycle.mjs ./cookbook-out

It also ships inside the npm package itself, at node_modules/@fabricorg/airlift/examples/local-lifecycle.mjs, in releases published in 0.16.3 and later.

Contributors working from a repository checkout can run it directly against the built package surface instead:

pnpm install
pnpm --filter @fabricorg/airlift build
node packages/airlift/examples/local-lifecycle.mjs ./cookbook-out

Configured authorization

The authorization directory is install-time configuration, not request input. Memberships bind natural persons to tenant roles; system and agent principals are admitted only through narrow trusted-id lanes.

const authorizationConfig = {
  memberships: [
    { organizationId: ORG, principal: 'operator@acme.example', principalType: 'natural_person', role: 'admin' },
    { organizationId: ORG, principal: 'approver@acme.example', principalType: 'natural_person', role: 'approver' },
  ],
  // System principals admitted to the narrow worker transition set.
  trustedWorkerIds: ['svc-airlift-worker'],
  // Agent principals admitted only to a bounded action set: assessment,
  // dependency mapping, plan generation, inventory, conversion, artifacts,
  // residue, discrepancy triage, and modernization recommendations. Agents can
  // NEVER approve, certify, mint certificates, or execute cutover.
  trustedAgentIds: ['agent-convert-01'],
  // System principals admitted to the platform registry bootstrap boundary.
  platformInstallerIds: ['svc-airlift-installer'],
  // Tenant-bound principals permitted to submit independently produced evidence.
  admittedValidationPrincipals: [
    { organizationId: ORG, principal: 'svc-airlift-worker', provider: 'experiments' },
  ],
};

The runtime composes that directory with evidence services and an Ed25519 signer:

const runtime = createAirliftRuntime({
  authorization: createAirliftConfiguredAuthorization(authorizationConfig),
  services: {
    // Admits a validation run only when producer, provider, run ref, evidence
    // ref, and digest all match an immutable registry entry.
    ...configuredAirliftEvidenceServices(authorizationConfig, [evidenceRegistryEntry]),
    airliftEvidenceSigner: createEd25519EvidenceSigner({ keyId, privateKeyPem }),
  },
});

Platform-tenant bootstrap

Organizations are minted from the platform registry tenant, never from an org tenant. airlift.org_provision requires all three of:

  • tenantId = AIRLIFT_PLATFORM_TENANT ('airlift-platform'),
  • a system actor listed in platformInstallerIds,
  • { orgId, name } params.
await runtime.invokeAction(AIRLIFT_ACTION_IDS.orgProvision, {
  tenantId: AIRLIFT_PLATFORM_TENANT,
  actorId: 'svc-airlift-installer',
  actorType: 'system',
  params: { orgId: 'acme-analytics', name: 'Acme Analytics' },
  idempotencyKey: 'cookbook:org',
});

Invoking it with the org's own tenant id fails with Module "airlift" is not enabled for tenant …. That message reads like a defect but is the installer boundary working as designed: the installer grant is tenant-bound, so entitlement fails closed outside the platform tenant.

Happy-path ordering

Each stage lists its acting principal; prerequisites are explicit. The runnable script contains the full parameter shapes for every call.

  1. Estateestate_register (operator). environment: 'prod' engages the full gate policies.
  2. Engagementengagement_create with services: ['discovery', 'migration_factory'], then engagement_activate (operator). Discovery and factory actions require an active engagement.
  3. Assessmentassessment_record (operator) with tool version, report refs, digests, and object counts.
  4. Inventory + dependency graph acceptobject_register per object (operator); dependency_graph_start and dependency_batch_record (trusted agent); dependency_graph_accept with an explicit expectedEdgeCount, then assessment_accept with expectedObjectCount (operator). Acceptance freezes scope digests.
  5. Plan generate → select → freezeplan_generate (dependency-aware wave candidates plus economics), plan_select, then engagement_freeze before plan_freeze (operator) — freezing a plan requires the engagement scope frozen first.
  6. Wave plan/assignwave_plan, wave_assign per object (operator); objects reach planned.
  7. Conversion batchconversion_batch_create (operator), conversion_batch_start (worker), then per object conversion_start / conversion_record (worker). Every converted object must carry an immutable target_code artifact (artifact_register) whose digest matches the recorded outputDigest.
  8. Residue lane — for each failed conversion: residue_create → residue_estimate (agent) → residue_assign (operator). The batch then completes with completed_with_residue — completion requires every failed object to hold an active residue case. Remediation continues after completion: artifact_register + residue_resolve (operator), residue_review by a distinct natural person (approver), then a fresh governed conversion_start / conversion_record brings the repaired object from rework back to converted.
  9. Validation evidence admissionvalidation_profile_assign (operator), then validation_run_record (admitted principal). The run is admitted only because producer, provider, run ref, evidence ref, and digest all match the immutable registry entry — a caller's digest is never trusted merely because it is well formed.
  10. Readiness tracksreadiness_record per required track of the assigned profile, admitting the validation run as evidence.
  11. Parity certifyparity_certify with honest depth fields (see below).
  12. Business acceptancebusiness_accept is a human decision by the approver.
  13. Migration certificate mintmigration_certificate_mint binds the certificate to the exact expectedProfileDigest and expectedReadinessDigest the caller observed; the Ed25519 signer signs the envelope digest.
  14. Wave approvalwave_approve by the approver (a natural person distinct from the operator).
  15. Offline verification — the script verifies the envelope in-process, exports the envelope and public key, and prints the CLI command for you to run, fully offline:
fa certificate verify ./cookbook-out/migration-certificate.json --keys ./cookbook-out/verify-keys.json

Evidence export

airlift.evidence_export assembles the replayable evidence pack for one wave (or one migration object) from the org's event log — assessment, conversions with tool versions, parity certificates with declared evidence depth, approvals, and the governance revision in force at export time. It returns the pack as reportJson alongside a sha256 reportDigest over that exact string, and records the export itself as an AirliftEvidenceExported event keyed by that digest — the audit stream proves what was attested without persisting the pack contents.

The params contract requires a waveId or objectId plus a reason (1–500 characters):

const result = await runtime.invokeAction(AIRLIFT_ACTION_IDS.evidenceExport, {
  tenantId: ORG,
  actorId: 'operator@acme.example',
  actorType: 'natural_person',
  params: { waveId, reason: 'Assemble the replayable evidence pack for the approved wave.' },
  idempotencyKey: 'cookbook:evidence-export',
});
// result.data: { waveId, exportKey, reportDigest, reason, reportJson }

Authorization requires the airlift:evidence:export permission, held by the admin, operator, approver, and validator roles for natural-person principals (the cookbook's operator is configured as admin). Evidence export is deliberately not a trusted-worker action — unlike the conversion and certification steps above, a system principal is denied even though it drove most of the lifecycle. The cookbook demonstrates this: the conversion worker's attempt fails closed with the same generic entitlement error as the other module-boundary denials, before the operator invokes the action successfully.

Expected denials are first-class outcomes

A suite of passing checks behaves identically with the boundary disabled, so the cookbook demonstrates each denial and fails if any of them unexpectedly succeeds:

AttemptOutcome
org_provision from the org's own tenantEntitlement fails closed: Module "airlift" is not enabled for tenant …
Agent actor invoking wave_approveEntitlement fails closed with the same generic error — approvals are outside the bounded agent action set, so the agent is not entitled to the module for this action at all
Parity certificate claiming sampled_rows depth without aggregateChecksumsPolicy blocks the overclaim: a certificate must carry the evidence for every depth it claims
Resolver reviewing their own residuePolicy blocks it: The resolver cannot independently review the same residue.
System worker invoking evidence_exportEntitlement fails closed with the same generic error — evidence export is natural-person-only and deliberately not a trusted-worker action

Idempotency keys

Every mutation passes an idempotencyKey following a stable <demo>:<step>[:<subject>] convention (for example cookbook:cnv-record:<objectId>). Within the same runtime, re-invoking with the same key collapses onto the original invocation instead of duplicating the logical command — the cookbook demonstrates this by registering the estate twice and asserting both calls return the same estate id. The in-memory store lives only as long as the process, so a rerun of the script starts from scratch; against the durable Postgres store the same keys give cross-process replay safety.

The pre-cutover boundary

The local walkthrough stops at certified objects and an approved wave. It deliberately does not claim cutover execution or rehearsal, data transfer, workspace job runs, deployment evidence, or production operational evidence. Those stages require the durable runtime, real effectors, and independently produced evidence — a local in-memory demo proving them would be an overclaim, which is exactly what the governance model exists to prevent.

On this page