> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build self-service infrastructure

> Expose a clean, parameterized interface so developers deploy production-ready infrastructure with a few flags while your code enforces the standards.

As a platform team you want developers to get production-ready infrastructure
without touching the code that defines it. formae lets you do this with
[properties](/documentation/concepts/properties): you write one parameterized
forma, expose only the inputs developers should choose, and every property
becomes a CLI flag automatically. Developers run `formae apply` with a few
flags, and your code turns those choices into compliant infrastructure.

This guide walks through building such an offering: define the interface, map
choices to real infrastructure, bake in your standards, and hand it to
developers.

## Design the developer interface

Properties are the contract between you and your developers. Declare only the
inputs they need to vary as members of a typed `Props` class. Each member's name
becomes a `--flag` CLI option automatically. A member with a `default` is
optional; a member without one is required.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
// team-database.pkl
extends "@formae/forma.pkl"
import "@formae/formae.pkl"

properties: Props

class Props {
  /// Owning team
  team: String

  /// Deployment environment
  env: String = "dev"

  /// Database size, as a t-shirt size
  size: String = "small"
}
```

Here `--team` is required, while `--env` and `--size` fall back to their
defaults. Developers never open the Pkl file; they interact entirely through
these flags.

<Note>
  Declaring properties as a typed class requires `extends "@formae/forma.pkl"`
  (not `amends`). See [Properties](/documentation/concepts/properties) for the
  typed form and the legacy `properties {}` block.
</Note>

<Tip>
  Use defaults generously. The fewer required flags, the easier the offering is to
  adopt. Make `--team` required because it has no sensible default, but let
  `--size` default to the common case.
</Tip>

## Map choices to infrastructure decisions

This is where your platform expertise lives: translating a small vocabulary like
`small`/`medium`/`large` into concrete instance classes, storage sizes, and
policies. Read the property values and derive the real configuration.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
// Map t-shirt sizes to instance config
local instanceClass = if (properties.size == "large") "db.r6g.xlarge"
  else if (properties.size == "medium") "db.r6g.large"
  else "db.t4g.micro"

local storageGb = if (properties.size == "large") 500
  else if (properties.size == "medium") 100
  else 20

local teamName = properties.team
local envName = properties.env
```

You can also constrain a `Props` member directly so a bad input fails before any
cloud API call: a closed enumeration (`size: "small" | "medium" | "large"`) or a
regex pattern rejects invalid values at evaluation time rather than mid-deploy.
See [Properties](/documentation/concepts/properties) for the constraint patterns.

## Bake your standards into the resources

Encode your organization's requirements directly in the forma. Developers get
compliant infrastructure without having to know the rules, because the rules are
in the code, not in a wiki.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
forma {
  new formae.Stack {
    label = "\(teamName)-\(envName)"
  }

  new formae.Target {
    label = "\(teamName)-target"
    config = new aws.Config {
      region = "us-east-1"
    }
  }

  new dbinstance.DBInstance {
    label = "\(teamName)-database"
    engine = "postgres"
    engineVersion = "15.4"
    dbInstanceClass = instanceClass
    allocatedStorage = storageGb.toString()
    storageEncrypted = true               // Always encrypted
    multiAZ = envName != "dev"            // Multi-AZ outside dev
    deletionProtection = envName == "production"
    masterUsername = teamName
    masterUserPassword = formae.value(random.password(24, true)).opaque.setOnce
    tags {
      new { key = "Team"; value = teamName }
      new { key = "Environment"; value = envName }
      new { key = "ManagedBy"; value = "formae" }
    }
  }
}
```

The generated password uses `.opaque.setOnce`: `.opaque` keeps it out of logs
and output, and `.setOnce` fixes it on the first apply so it stays stable across
later applies. See [Values](/documentation/concepts/values) for both modifiers.

<Note>
  These examples use AWS resources, but the same pattern applies to any technology
  that has a formae [plugin](/documentation/concepts/plugin). Properties and
  standards are provider-agnostic.
</Note>

## Encapsulate larger offerings with modules

For anything bigger than a single resource, group the pieces into a Pkl class so
the forma stays clean and the complexity lives behind an interface. A class
takes the same property values as inputs and returns a listing of resources.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
// infrastructure/team_environment.pkl
import "@aws/ec2/securitygroup.pkl"
import "@aws/rds/dbinstance.pkl"
import "@aws/s3/bucket.pkl"

class TeamEnvironment {
  team: String
  env: String
  size: String

  hidden sg: securitygroup.SecurityGroup = new {
    label = "\(team)-\(env)-sg"
    groupDescription = "\(team) \(env) security group"
    // ... ingress/egress rules baked in
  }

  hidden database: dbinstance.DBInstance = new {
    label = "\(team)-\(env)-db"
    // ... size mapping, encryption, tags
  }

  hidden artifacts: bucket.Bucket = new {
    label = "\(team)-\(env)-artifacts"
    bucketName = "\(team)-\(env)-artifacts"
    // ... versioning, lifecycle rules
  }

  resources: Listing = new {
    sg
    database
    artifacts
  }
}
```

The forma then reduces to wiring the class to the properties and spreading its
resources:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
import "./infrastructure/team_environment.pkl"

local environment = new team_environment.TeamEnvironment {
  team = properties.team
  env = properties.env
  size = properties.size
}

forma {
  new formae.Stack { label = "\(properties.team)-\(properties.env)" }
  new formae.Target {
    label = "default"
    config = new aws.Config { region = "us-east-1" }
  }
  ...environment.resources
}
```

## Hand it to developers

The properties you declared are already the whole interface. Developers discover
them by passing the forma to `--help`, then deploy with the flags.

<Steps>
  <Step title="Discover the available flags">
    Passing the forma file to `--help` lists the property flags, their defaults,
    and which are required:

    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae apply --help team-database.pkl
    ```

    The output includes a `Properties:` section, for example:

    <div className="fa-term">
      <div className="fa-term-body"><span style={{fontWeight:'600'}} /><span style={{color:'#ff7b72',fontWeight:'600'}}>Properties:</span><br />      --env                   property: env \[default: "dev"]<br />      --size                  property: size \[default: "small"]<br />      --team                  property: team \[required]</div>
    </div>
  </Step>

  <Step title="Deploy with the chosen values">
    A developer supplies the flags and applies. Reconcile mode brings the team's
    stack into being exactly as the forma (and their choices) describe it.
    formae shows the plan and asks for confirmation before making any changes,
    then streams the run in a live view:

    ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
    formae apply --mode reconcile --team payments --env staging team-database.pkl
    ```

    <div className="fa-term">
      <div className="fa-term-body">  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(232,232,232)',fontWeight:'600'}}>formae</span> <span style={{fontWeight:'600'}} /><span style={{color:'rgb(255,133,51)',fontWeight:'600'}}>apply · reconcile</span>                                                                       team-database.pkl<br /><span style={{color:'rgb(85,85,102)'}}>────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span><br /><span style={{color:'rgb(85,85,102)'}} />  <span style={{color:'rgb(232,232,232)'}}>+</span> 3 <span style={{color:'rgb(232,232,232)'}}>create</span><br /><br />  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(255,133,51)',fontWeight:'600'}}>▌ Targets</span><br />  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(232,232,232)',fontWeight:'600'}} /><span style={{color:'rgb(232,232,232)',fontWeight:'600'}}>Operation ▲   </span><span style={{color:'rgb(170,170,170)'}}>Label</span><br />  <span style={{color:'rgb(232,232,232)'}}>+ create      payments-target</span><br /><br />  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(255,133,51)',fontWeight:'600'}}>▌ Stacks</span><br />  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(232,232,232)',fontWeight:'600'}} /><span style={{color:'rgb(232,232,232)',fontWeight:'600'}}>Operation ▲   </span><span style={{color:'rgb(170,170,170)'}}>Label</span><br />  <span style={{color:'rgb(170,170,170)'}}>+ create      </span><span style={{color:'rgb(129,209,219)'}}>payments-staging</span><br /><br />  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(255,133,51)',fontWeight:'600'}}>▌ Resources</span><br />  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(232,232,232)',fontWeight:'600'}} /><span style={{color:'rgb(232,232,232)',fontWeight:'600'}}>Operation ▲   </span><span style={{color:'rgb(170,170,170)'}}>Label                                                       Type</span><br />  <span style={{color:'rgb(170,170,170)'}}>+ create      </span><span style={{color:'rgb(129,209,219)'}}>payments-database                                           </span><span style={{color:'rgb(170,170,170)'}}>AWS::RDS::DBInstance</span><br /><br /><span style={{color:'rgb(85,85,102)'}}>────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span><br /><span style={{color:'rgb(85,85,102)'}} />  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(129,209,219)',fontWeight:'600'}}>↑↓</span><span style={{color:'rgb(153,153,153)'}}>: select</span>  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(129,209,219)',fontWeight:'600'}}>space</span><span style={{color:'rgb(153,153,153)'}}>: expand</span>  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(129,209,219)',fontWeight:'600'}}>→←</span><span style={{color:'rgb(153,153,153)'}}>: column</span>  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(129,209,219)',fontWeight:'600'}}>s</span><span style={{color:'rgb(153,153,153)'}}>: sort</span>  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(129,209,219)',fontWeight:'600'}}>y</span><span style={{color:'rgb(153,153,153)'}}>: confirm</span>  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(129,209,219)',fontWeight:'600'}}>q</span><span style={{color:'rgb(153,153,153)'}}>: abort</span> This operation will create 1 stack(s), create<br />1 target(s) and create 1 resource(s).  Do you want to continue? (y/N)  <span style={{fontWeight:'600'}} /><span style={{color:'rgb(129,209,219)',fontWeight:'600'}}>?</span><span style={{color:'rgb(153,153,153)'}}>: help</span></div>
    </div>

    Pass `--yes` to skip the confirmation prompt in a CI/CD job.
  </Step>
</Steps>

<Note>
  Treat property flags as a public API. Renaming a `Props` member renames its
  `--flag` and breaks every developer script and CI job that passes it. Keep flags
  stable: add new members rather than renaming, and deprecate old ones gradually.
</Note>

## See also

* [Properties](/documentation/concepts/properties): the full reference for typed, constrained property interfaces.
* [Apply modes](/documentation/concepts/apply-modes): reconcile versus patch, and how each treats a stack.
* [Values](/documentation/concepts/values): the `.opaque` and `.setOnce` modifiers for secrets and stable generated values.
* [Write your first forma](/documentation/get-started/write-your-first-forma): a worked example of turning a hardcoded value into a property end to end.
