> ## 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.

# 03 - Target configuration

> Learn how a plugin receives target configuration, keep secrets out of targets, and parse the SFTP server URL.

This section explains how plugins receive target configuration.

## What is a target?

A **target** uniquely identifies a deployment location for your infrastructure. Think of it as "where" your resources live:

* For AWS: a region like `us-east-1`
* For Kubernetes: a cluster endpoint
* For SFTP: a server URL like `sftp://files.example.com:22`

Targets are stored in formae's database in **plaintext**, so they must never contain secrets. The target identifies the location; credentials are provided separately.

## Target vs credentials

|                    | Target                         | Credentials                     |
| ------------------ | ------------------------------ | ------------------------------- |
| **Purpose**        | Identifies deployment location | Authenticates to the location   |
| **Storage**        | Plaintext in database          | Environment variables / secrets |
| **Example (SFTP)** | `sftp://files.example.com:22`  | Username, password              |
| **Example (AWS)**  | `us-east-1`                    | Access key, secret key          |

## Target configuration in requests

Every request to your plugin includes a `TargetConfig` field:

```go theme={"languages":{"custom":["/languages/pkl.json"]}}
type CreateRequest struct {
    ResourceType string
    Properties   json.RawMessage
    TargetConfig json.RawMessage  // <-- Target configuration (no secrets!)
}
```

## Define the target config type

For our SFTP plugin, the target is just the server URL. Add this to `sftp.go`:

```go theme={"languages":{"custom":["/languages/pkl.json"]}}
// TargetConfig holds SFTP target settings.
// Contains only the deployment location, NOT credentials.
type TargetConfig struct {
    URL string `json:"url"` // sftp://host:port
}

func parseTargetConfig(data json.RawMessage) (*TargetConfig, error) {
    var cfg TargetConfig
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("invalid target config: %w", err)
    }
    if cfg.URL == "" {
        return nil, fmt.Errorf("target config missing 'url'")
    }
    return &cfg, nil
}

func parseURL(sftpURL string) (host string, port string, err error) {
    u, err := url.Parse(sftpURL)
    if err != nil {
        return "", "", fmt.Errorf("invalid URL: %w", err)
    }
    if u.Scheme != "sftp" {
        return "", "", fmt.Errorf("expected sftp:// URL, got %s://", u.Scheme)
    }
    host = u.Hostname()
    port = u.Port()
    if port == "" {
        port = "22"
    }
    return host, port, nil
}
```

## Update sftp.go

Add the target configuration and credentials code. Update the imports and add the helper functions:

```go theme={"languages":{"custom":["/languages/pkl.json"]}}
package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "net/url"
    "os"

    "github.com/platform-engineering-labs/formae/pkg/plugin"
    "github.com/platform-engineering-labs/formae/pkg/plugin/resource"
)

// TargetConfig holds SFTP target settings.
// Contains only the deployment location, NOT credentials.
type TargetConfig struct {
    URL string `json:"url"` // sftp://host:port
}

func parseTargetConfig(data json.RawMessage) (*TargetConfig, error) {
    var cfg TargetConfig
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("invalid target config: %w", err)
    }
    if cfg.URL == "" {
        return nil, fmt.Errorf("target config missing 'url'")
    }
    return &cfg, nil
}

func parseURL(sftpURL string) (host string, port string, err error) {
    u, err := url.Parse(sftpURL)
    if err != nil {
        return "", "", fmt.Errorf("invalid URL: %w", err)
    }
    if u.Scheme != "sftp" {
        return "", "", fmt.Errorf("expected sftp:// URL, got %s://", u.Scheme)
    }
    host = u.Hostname()
    port = u.Port()
    if port == "" {
        port = "22"
    }
    return host, port, nil
}

func getCredentials() (username, password string, err error) {
    username = os.Getenv("SFTP_USERNAME")
    password = os.Getenv("SFTP_PASSWORD")
    if username == "" || password == "" {
        return "", "", fmt.Errorf("SFTP_USERNAME and SFTP_PASSWORD must be set")
    }
    return username, password, nil
}

// ... rest of Plugin implementation
```

## Config field mutability

You can annotate target config fields to tell formae which fields can be changed in place and which require a full target replace. Add `@formae.ConfigFieldHint` annotations to your PKL Config class:

```kotlin theme={"languages":{"custom":["/languages/pkl.json"]}}
import "@formae/formae.pkl"

open class Config {
  hidden fixed type: String = "SFTP"

  /// The server URL. Changing this requires recreating everything.
  @formae.ConfigFieldHint { createOnly = true }
  hidden url: String

  fixed Type: String = type
  fixed URL: String = url
}
```

* `createOnly = true`: immutable, triggers target replace (default for unannotated fields)
* `createOnly = false`: mutable, allows in-place update

For our SFTP plugin, the URL is the only config field and it identifies the deployment location, so it should be immutable. For plugins with credential-like config fields (e.g. a named profile), marking those as `createOnly = false` lets users switch credentials without recreating resources.

See the [schema annotations reference](/plugin-development/reference/schema) for full details.

## Verify

To verify that the target configuration code compiles correctly:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
cd formae-plugin-sftp
make build
```

***

Next: [04 - Plugin configuration](/plugin-development/tutorial/04-configuration)

## See also: advanced target patterns

For Target Config patterns beyond this introduction:

* **Cross-plugin Target Resolvables** (Target Config field accepting upstream resolvable from another plugin's resource): see the Grafana plugin's Target Config, which references AWS/Compose resolvables.
* **Collection Resolvables** (`Mapping<String, formae.Resolvable>` and similar): see the Compose plugin's Target Config.
* **Polymorphic Target auth** (abstract auth class plus KubeconfigAuth/EKSAuth/GKEAuth/... discriminated by `hidden fixed type`): see the Kubernetes plugin's Target Config.
