Skip to main content

Quick Start with Infrastructure as Code

From an empty account to a running Live System, entirely from code you can commit and run in a pipeline.

This page uses the TypeScript SDK. The Java SDK and the Terraform Provider express the same five concepts with language-idiomatic spellings — see equivalents in other languages at the end.

Before you start
  • A Fractal Cloud account.
  • Administrator access to one AWS account, Azure subscription, or GCP project.
  • Node.js 18+.

See prerequisites for detail.

Step 1 — Install the SDK

npm install @fractal_cloud/sdk

Everything on this page imports from the locked model surface:

import {/* ... */} from '@fractal_cloud/sdk/model';

Step 2 — Get your API credentials

Your code authenticates to Fractal Cloud with a Client Id and Client Secret belonging to the environment's CI/CD service account.

  1. Open your environment in the dashboard.
  2. Copy the CI/CD service account's Client Id and Client Secret.
  3. Supply them to your application or pipeline as you would any other credential.

This is identical for every cloud provider, so your SDK setup does not change between AWS, Azure, GCP, or the enterprise providers.

export SERVICE_ACCOUNT_ID="..."
export SERVICE_ACCOUNT_SECRET="..."
export OWNER_ID="..." # your account or organization UUID

The client is built once and holds those credentials:

import {createFractalCloudClient} from '@fractal_cloud/sdk/model';

const cloud = createFractalCloudClient({
clientId: process.env['SERVICE_ACCOUNT_ID']!,
clientSecret: process.env['SERVICE_ACCOUNT_SECRET']!,
});
Looking for these in your own cloud secret store?

Earlier versions of Fractal Cloud also copied this credential into your own cloud secret store — Azure Key Vault, AWS Secrets Manager, GCP Secret Manager, OCI Vault or Hetzner Secret Manager — as fractal-ci-cd-service-account-name and fractal-ci-cd-service-account-password.

That copy is no longer written, and it is removed from environments that still hold one the next time their Cloud Agent is updated. Use the dashboard instead: it is the single authoritative source, so it cannot fall out of step with the credential the platform actually accepts. The service account itself is untouched.

Step 3 — Get an environment with a Cloud Agent

Before any code can deploy anything, you need an Environment whose Cloud Agent is initialized in your cloud account. The agent runs inside your account and does the reconciling — the Fractal Cloud control plane holds no standing access to your cloud.

Two ways to get one. Both end with the same thing, and the rest of this page is identical either way.

Follow UI quick start steps 1–3: create a Bounded Context, create an environment, initialize the Cloud Agent. Two minutes, and the provider authorization flow is guided.

Then reference it from code as a plain object — no environment declaration needed:

const environment = {
ownerType: 'Personal',
ownerId: process.env['OWNER_ID']!,
name: 'dev', // the environment's short name
};

Option B — declare and deploy it in code

For repeatable bootstrap, declare the environment tree yourself. Environments come in two tiers:

TierDeclares
ManagementThe Cloud Agent with full identity — tenant / organization / tenancy plus a subscription / account / project. Owns the operational environments beneath it.
OperationalOnly a cloud account per provider. The tenant / organization / tenancy is inherited from the matching management agent when the tree resolves.
environment.ts
import {
ManagementEnvironment,
OperationalEnvironment,
} from '@fractal_cloud/sdk/model';

const OWNER_ID = process.env['OWNER_ID']!;

// Where Live Systems land. Declares a subscription; inherits the tenant.
const prod = OperationalEnvironment({
shortName: 'prod',
resourceGroups: [`Personal/${OWNER_ID}/prod-rg`],
}).withAzureSubscription({
region: 'westeurope',
subscriptionId: process.env['AZURE_OPERATIONAL_SUBSCRIPTION_ID']!,
});

// Owns the Cloud Agent and the operational environments beneath it.
export const management = ManagementEnvironment({
id: {type: 'Personal', ownerId: OWNER_ID, shortName: 'mgmt'},
resourceGroups: [`Personal/${OWNER_ID}/mgmt-rg`],
})
.withAzureCloudAgent({
region: 'westeurope',
tenantId: process.env['AZURE_TENANT_ID']!,
subscriptionId: process.env['AZURE_MANAGEMENT_SUBSCRIPTION_ID']!,
})
.withOperationalEnvironments([prod]);

Deploying the tree creates or updates both environments, pushes their secrets and CI/CD profiles, and initializes the Cloud Agents. agentInit: 'wait' blocks until each initialization completes:

await cloud.environments.deploy(management, {
agentInit: 'wait',
providerCredentials: {
azure: {
spClientId: process.env['AZURE_SP_CLIENT_ID']!,
spClientSecret: process.env['AZURE_SP_CLIENT_SECRET']!,
},
},
});

Then target the operational environment by name — a typo throws instead of deploying somewhere unexpected:

const environment = management.operational('prod').ref();

Use management.ref() to target the management environment itself.

Swap the cloud by swapping the agent and the account: .withAwsCloudAgent / .withGcpCloudAgent / .withOciCloudAgent / .withHetznerCloudAgent, and .withAwsAccount / .withGcpProject / .withOciCompartment / .withHetznerProject. Every field is documented in the Environments reference; the cloud-side prerequisites per provider — initialization roles, service principals, service accounts — are in Advanced initialization.

Option B claims cloud accounts

Deploying an environment tree initializes a real Cloud Agent in each tier, so two subscriptions or accounts get claimed — and they must be two distinct, unclaimed ones. The initialization principal also needs broad provisioning rights, which differ per scope. Missing grants do not fail early: the claim succeeds and the run fails later, partway through provisioning, looking like a credential problem. Read the provider guide before running this.

Credentials are never implicit

providerCredentials is read only from the call site — never from ambient environment variables. A deployment that needs to initialize a provider with no credentials supplied fails rather than silently picking up whatever is in the shell. Federated (OIDC) variants are accepted for AWS, Azure, and GCP, so CI never needs a long-lived secret.

Step 4 — Author a Fractal

A Fractal is the reusable blueprint: which components exist and how they relate. It references abstract components only — ObjectStorage, not AwsS3 — which is what keeps it satisfiable by any vendor.

fractal.ts
import {createFractal, ObjectStorage} from '@fractal_cloud/sdk/model';

const boundedContextId = {
ownerType: 'Personal',
ownerId: process.env['OWNER_ID']!,
name: 'reusable-templates',
};

export function authorFractal() {
return createFractal({
id: 'acme-payments',
version: {major: 1, minor: 0, patch: 0},
description: 'A single governed uploads bucket.',
boundedContextId,
blueprint: bp => {
const uploads = bp.add(
ObjectStorage({id: 'uploads', displayName: 'Uploads Bucket'})
.withEncryption('at-rest') // guardrail: always encrypted
.withPublicAccess(false) // guardrail: never public
.withVersioningEnabled(true) // guardrail: keep object history
);
return {uploads};
},

// The typed interface a consuming team may use — application-level verbs only.
operations: s => ({
/** The application's bucket folder layout. */
withFolders: (folders: string[]) => s.uploads.set('folders', folders),
}),
});
}

Two kinds of specialization live in that file, and the distinction is the whole point:

  • Guardrails — every .withXxx() the architect calls at design time records a parameter and locks it. withPublicAccess(false) can never be turned back on by a consuming team; trying throws before any network call.
  • Operations — the typed interface, and the only customization a consumer may perform. They are application-level verbs (which folders the app writes to, which image it ships), not pass-through setters for infra knobs.

See guardrails vs dev-open parameters.

The returned Fractal is immutable: .specialize() never mutates it, so authoring once and instantiating many times is safe.

Step 5 — Select offers and build a Live System

A Live System is your instantiation: application intent applied through the operations, then one concrete offer picked per abstract component. This is where the vendor gets chosen.

deploy.ts
import {AwsS3} from '@fractal_cloud/sdk/model';
import {authorFractal} from './fractal';

const fractal = authorFractal();

const liveSystem = fractal
.specialize()
.withFolders(['invoices', 'exports']) // an operation from the Fractal interface
.toLiveSystem({
name: 'acme-uploads',
environment, // from Step 3
select: {
uploads: AwsS3({region: 'eu-west-1'}),
},
});

select is type-checked against the blueprint: an unknown component id, a missing component, or an offer that does not satisfy that component's contract is a compile error, and an unknown key is rejected at runtime too.

Swapping AwsS3({region: 'eu-west-1'}) for AzureBlob({accountTier: 'Standard_LRS'}) or GcsBucket({region: 'EU'}) is the only change needed to run the same Fractal on another cloud. Every offer and its parameters are in the Component Reference, one page per vendor.

Step 6 — Publish the blueprint, then deploy

Blueprints and Live Systems are separate entities. Register the reusable, vendor-agnostic blueprint first — the API rejects a Live System whose Fractal is not registered.

deploy.ts
await cloud.blueprints.create(fractal);
await cloud.liveSystems.deploy(liveSystem, {mode: 'wait'});

blueprints.create is an idempotent upsert, so re-running a deployment script is safe. Pass {isPrivate: true} to keep the blueprint out of the shared catalogue. Deploying a Live System never publishes a blueprint as a side effect, so a blueprint change stays a deliberate, reviewable act.

mode: 'wait' polls until the Live System is Active and emits an append-only log with no ANSI escapes — safe for any CI log aggregator:

[2026-03-10T14:23:01Z] INFO Deploying Live System system=<id> fractal=<id> provider=AWS
[2026-03-10T14:23:11Z] CHECK Polling Live System status system=<id> round=1 status=<status> elapsed=10s
[2026-03-10T14:23:31Z] INFO Live System Active system=<id> elapsed=30s

mode: 'fire-and-forget' submits and returns immediately, emitting no logs. Use it when a pipeline should not block on provisioning. Full options — quiet, pollIntervalMs, timeoutMs — are in DeployOptions.

Run it:

npx tsx deploy.ts

Rerunning is safe. The agent reconciles: it applies only the delta between what you declared and what is actually in the cloud, correcting drift. There is no state file to keep — your cloud is the source of truth.

Never print a raw error from a failed call

Credentials travel to the API as HTTP headers, and Node's inspection of a failed request walks the raw request header block — so a bare main().catch(err => console.error(err)) prints your service-account secret in full. The likeliest failure on a first run is exactly the one that triggers it: a 401 or 403 from a mistyped credential. The sample repositories ship a fatal.ts that redacts before reporting; copy it rather than writing your own catch.

Step 7 — Read what came out

Components publish output fields: vendor-agnostic facts a consumer needs, identical in shape across clouds.

const state = await cloud.liveSystems.outputs(liveSystem);
console.log(state.status); // e.g. 'Active'
console.log(state.components['uploads'].outputFields);

Output fields never contain raw secrets — only *_REF references, which a workload runtime resolves from the vendor secret store at launch.

The whole thing

Assuming an environment created in the dashboard (Step 3, Option A):

deploy.ts
import {
createFractal,
createFractalCloudClient,
ObjectStorage,
AwsS3,
} from '@fractal_cloud/sdk/model';
import {fatal} from './fatal';

const OWNER_ID = process.env['OWNER_ID']!;

const cloud = createFractalCloudClient({
clientId: process.env['SERVICE_ACCOUNT_ID']!,
clientSecret: process.env['SERVICE_ACCOUNT_SECRET']!,
});

const fractal = createFractal({
id: 'acme-payments',
version: {major: 1, minor: 0, patch: 0},
description: 'A single governed uploads bucket.',
boundedContextId: {
ownerType: 'Personal',
ownerId: OWNER_ID,
name: 'reusable-templates',
},
blueprint: bp => {
const uploads = bp.add(
ObjectStorage({id: 'uploads', displayName: 'Uploads Bucket'})
.withEncryption('at-rest')
.withPublicAccess(false)
.withVersioningEnabled(true),
);
return {uploads};
},
operations: s => ({
withFolders: (folders: string[]) => s.uploads.set('folders', folders),
}),
});

async function main() {
const liveSystem = fractal
.specialize()
.withFolders(['invoices', 'exports'])
.toLiveSystem({
name: 'acme-uploads',
environment: {ownerType: 'Personal', ownerId: OWNER_ID, name: 'dev'},
select: {uploads: AwsS3({region: 'eu-west-1'})},
});

await cloud.blueprints.create(fractal);
await cloud.liveSystems.deploy(liveSystem, {mode: 'wait'});

const state = await cloud.liveSystems.outputs(liveSystem);
console.log(state.components['uploads'].outputFields);
}

main().catch(fatal);
Congratulations 🎉

Real infrastructure, provisioned from a governed blueprint, in a file you can commit and review. Put deploy.ts behind a pipeline and it is your deployment.

Tearing it down

await cloud.liveSystems.destroy(liveSystem);

The blueprint stays registered — destroying a Live System never unpublishes the Fractal it came from.

Equivalents in other languages

All three surfaces drive the same five concepts — Fractal, Component, Offer, Live System, Client.

SDKLanguagePackageSamples
TypeScript SDKTypeScript / Node.js@fractal_cloud/sdkSamples
Java SDKJavafractal-java-sdkSamples
Terraform ProviderHCLfractalcloud/fcSamples

Two TypeScript samples are the runnable versions of this page: basic_storage for Steps 4–7, and basic_environment for Step 3 Option B — it is the only sample that exercises the environment surface, and its README carries the full per-provider prerequisites.

What next

  • Deploy the same Fractal to a second environment — change only environment. Same blueprint, no edits.
  • Deploy it to a different cloud — change only the offer in select.
  • Full tutorial — a three-tier stack (managed cluster + PostgreSQL + web workload) with the Ops / Platform / Developer split.
  • Core API reference — authoring, guardrails, specialization, publishing, deployment modes, extending the catalogue with your own components and offers.
  • Link Settings — how to wire components to each other: database access, object-storage grants, identity providers, messaging.
  • See it in the dashboard — Fractals authored in code appear in the catalogue, and Live Systems deployed from code are visible and inspectable there.

If you get stuck, contact us.