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

# Build the module from the generated code

> Move the generated code into the shared module and turn per-environment values into variables.

Move each block from `generated.tf` into the module, then swap the values that differ per environment for variables.

For example, the generator writes the Email OTP block like this, with every optional attribute listed and the dev webhook URL hardcoded:

```hcl theme={null}
# scratch/generated.tf (trimmed)
resource "authsignal_email_otp_authenticator_configuration" "email_otp" {
  bird_email_credentials     = null
  email_provider             = "WEBHOOK"
  is_active                  = true
  mailgun_email_credentials  = null
  mailjet_email_credentials  = null
  mandrill_email_credentials = null
  sendgrid_email_credentials = null
  smtp_email_credentials     = null
  webhook_url                = "https://api.dev.example.com/authsignal/email-otp"
}
```

In the module, the null lines are gone and the URL is a variable. Its dev value moves to `envs/dev/terraform.tfvars`.

```hcl theme={null}
# modules/authsignal/authenticators.tf
resource "authsignal_email_otp_authenticator_configuration" "email_otp" {
  email_provider = "WEBHOOK"
  is_active      = true
  webhook_url    = var.email_otp_webhook_url
  # ...every other value from generated.tf that isn't null
}
```

The steps below do this for each block.

## Prerequisites

* `generated.tf` from [Generate code from the dev tenant](/knowledge-base/terraform/generate-code).

## 1. Delete the null lines

The generator writes out every optional attribute, so most blocks contain a lot of `= null` lines. Setting an attribute to null is the same as leaving it out, so delete those lines first. The theme block shrinks from around fifty lines to the handful you set in the portal.

Keep every line that has a value, including settings you never changed in the portal, such as the passkey's `user_verification_requirement` or the Email OTP rate limits. A setting that isn't in the module isn't copied to the other tenants, which get Authsignal's default instead.

## 2. Add `versions.tf`

The module needs its own `required_providers` block. Without it, `terraform init` fails looking for a provider called `hashicorp/authsignal`.

```hcl theme={null}
# modules/authsignal/versions.tf
terraform {
  required_version = ">= 1.11"
  required_providers {
    authsignal = {
      source  = "authsignal/authsignal"
      version = ">= 3.12"
    }
  }
}
```

These are the lowest versions the module works with. The environment folders choose the exact versions.

## 3. Authenticators

The generated code has three dev values hardcoded: the webhook URL, the passkey relying party, and the expected origins. Each becomes a variable. For example, `webhook_url = "https://api.dev.example.com/authsignal/email-otp"` becomes `webhook_url = var.email_otp_webhook_url`, and the URL moves to `envs/dev/terraform.tfvars`.

Keep `is_active = true` as generated. It matters when Terraform creates the authenticator in a new tenant.

```hcl theme={null}
# modules/authsignal/authenticators.tf
resource "authsignal_email_otp_authenticator_configuration" "email_otp" {
  email_provider = "WEBHOOK"
  is_active      = true
  webhook_url    = var.email_otp_webhook_url
  # ...every other value from generated.tf that isn't null
}

resource "authsignal_passkey_authenticator_configuration" "passkey" {
  is_active        = true
  relying_party    = var.passkey_relying_party
  expected_origins = var.passkey_expected_origins
  # ...every other value from generated.tf that isn't null
}
```

```hcl theme={null}
# modules/authsignal/variables.tf
variable "email_otp_webhook_url" {
  type = string
}

variable "passkey_relying_party" {
  type = string
}

variable "passkey_expected_origins" {
  type = set(string)
}

variable "tenant_name" {
  type = string
}
```

## 4. The flow

The API takes the flow as JSON, and `jsonencode` turns the HCL inside it into that JSON. Keep the block as generated. Leave the node and rule IDs alone, such as the node ID `rule-b3khtnzl` and the rule ID `8b82a51b-607b-4dfe-816f-a932552b24dd`. They travel with the flow, and Terraform creates the same IDs in the other tenants.

Terraform normally works out the order to create resources from the references between them. The flow doesn't reference the authenticators, so add `depends_on` to tell Terraform to create them first. Without it, Terraform may create the flow in a new tenant before the authenticators it uses.

```hcl theme={null}
# modules/authsignal/flows.tf
resource "authsignal_flow" "sign_in" {
  action_code = "sign-in"
  flow = jsonencode({
    actionNodes = [{
      nodeId           = "rule-b3khtnzl"
      nodeType         = "RULE"
      parentNodeIds    = []
      ruleChildNodeIds = [["8b82a51b-607b-4dfe-816f-a932552b24dd", "block-60f58fy8"]]
      elseChildNodeId  = "verify-v48mxwtl"
      }, {
      nodeId        = "block-60f58fy8"
      nodeType      = "BLOCK"
      parentNodeIds = ["rule-b3khtnzl"]
      }, {
      nodeId        = "verify-v48mxwtl"
      nodeType      = "VERIFICATION"
      name          = "verify"
      parentNodeIds = ["rule-b3khtnzl"]
      methodConfigurations = {
        EMAIL_OTP = { isEnabled = true }
        PASSKEY   = { isEnabled = true }
      }
      childNodeId = "complete-158a588m"
      }, {
      nodeId        = "complete-158a588m"
      nodeType      = "COMPLETE"
      parentNodeIds = ["verify-v48mxwtl"]
    }]
    rules = [{
      ruleId = "8b82a51b-607b-4dfe-816f-a932552b24dd"
      name   = "IP is anonymous"
      conditions = {
        and = [{ "==" = [{ var = "ip.isAnonymous" }, true] }]
      }
    }]
  })

  depends_on = [
    authsignal_email_otp_authenticator_configuration.email_otp,
    authsignal_passkey_authenticator_configuration.passkey,
  ]
}
```

## 5. The theme

Keep every value you set in the portal. They make the other tenants look identical. See [Branding](/implementation-options/prebuilt-ui/custom-branding) for what each setting does. The exception is `name`, which is the tenant name users see in the pre-built UI, so it becomes a variable.

```hcl theme={null}
# modules/authsignal/theme.tf
resource "authsignal_theme" "theme" {
  name          = var.tenant_name
  logo_url      = "https://cdn.example.com/logo.png"
  primary_color = "#2563EB"

  colors = {
    button_secondary_background = "#DBEAFE"
  }

  borders = {
    button_border_radius    = 31
    container_border_radius = 50
  }

  page_background = {
    background_color = "#EFF6FF"
  }
}
```

## 6. Message overrides

The generated block can usually be used as-is. See [Message overrides](/implementation-options/prebuilt-ui/message-overrides) for how overrides work. Keys are the screen ID, a dot, then the part of the screen, grouped by language code.

```hcl theme={null}
# modules/authsignal/messages.tf
resource "authsignal_message_overrides" "messages" {
  overrides = {
    en = {
      "email-otp-code-entry.heading"     = "hello world"
      "email-otp-code-entry.description" = "the quick brown fox jumps over the lazy dog"
    }
    fr = {
      "email-otp-code-entry.heading"     = "abcde"
      "email-otp-code-entry.description" = "edcba"
    }
  }
}
```

This resource holds the complete set of overrides for the tenant. An override added in another tenant, and not in the module, is removed on that tenant's next apply.

The full list of keys and default text is under **Settings → Pre-built UI → Message overrides** in the portal. The [`authsignal_message_overrides`](https://registry.terraform.io/providers/authsignal/authsignal/latest/docs/resources/message_overrides) page on the Terraform Registry describes the resource.

## 7. Delete the scratch folder

Everything it produced is now in the module.

## Verify

From the repository root, tidy the formatting and check the code:

```bash theme={null}
terraform fmt -recursive
cd envs/dev
terraform init
terraform validate
```

`fmt` fixes spacing and alignment after your edits. `validate` checks for typos and mismatched variable names without contacting the tenant. It should print `Success! The configuration is valid.`

[Import the dev tenant into Terraform](/knowledge-base/terraform/import-dev) then checks the module against the dev tenant.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.