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

# How the repository is set up

> The shared module, the per-environment folders, and how Terraform connects to each Authsignal tenant.

## The pieces

Each environment (dev, QA, and production) has its own tenant in the Authsignal Portal. All of the Authsignal configuration lives in one shared module, and every environment uses the same module. Each environment has a small folder that applies the module to its tenant.

A few values differ per tenant, such as the domain passkeys belong to: `dev.example.com` in dev and `example.com` in production. These are variables, and each environment sets them in its `terraform.tfvars` file. Each environment also has its own state, which is Terraform's record of the tenant resources it manages.

## Repository layout

```text theme={null}
authsignal-terraform/
  modules/authsignal/
    versions.tf         # tells Terraform where the provider comes from
    authenticators.tf   # Email OTP and passkey configuration
    flows.tf            # the sign-in flow
    theme.tf            # branding for the pre-built UI
    messages.tf         # message overrides
    variables.tf        # the few things that differ per tenant
  envs/
    dev/
      main.tf
      variables.tf
      terraform.tfvars
      .terraform.lock.hcl
    qa/
    production/
```

The module can be generated from the dev tenant, as in [Generate code from the dev tenant](/knowledge-base/terraform/generate-code) and [Build the module from the generated code](/knowledge-base/terraform/build-module), or written by hand.

Paths in these guides are relative to the repository root. Commands that start with `cd` assume you're at the root.

## Environment files

These are the files in `envs/dev`. Every environment has the same `main.tf` and `variables.tf`, apart from the backend settings described in [Where state is stored](#where-state-is-stored). Only the values in `terraform.tfvars` differ.

```hcl theme={null}
# envs/dev/main.tf
terraform {
  required_version = ">= 1.11"
  required_providers {
    authsignal = {
      source  = "authsignal/authsignal"
      version = "~> 3.12"
    }
  }
  # A backend block goes here. See Where state is stored.
}

provider "authsignal" {} # reads the AUTHSIGNAL_ environment variables

module "authsignal" {
  source = "../../modules/authsignal"

  tenant_name              = var.tenant_name
  email_otp_webhook_url    = var.email_otp_webhook_url
  passkey_relying_party    = var.passkey_relying_party
  passkey_expected_origins = var.passkey_expected_origins
}
```

```hcl theme={null}
# envs/dev/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
}
```

```hcl theme={null}
# envs/dev/terraform.tfvars
email_otp_webhook_url    = "https://api.dev.example.com/authsignal/email-otp"
passkey_relying_party    = "dev.example.com"
passkey_expected_origins = ["https://dev.example.com"]
tenant_name              = "Example (dev)"
```

```hcl theme={null}
# envs/production/terraform.tfvars
email_otp_webhook_url    = "https://api.example.com/authsignal/email-otp"
passkey_relying_party    = "example.com"
passkey_expected_origins = ["https://example.com", "https://app.example.com"]
tenant_name              = "Example"
```

`version = "~> 3.12"` allows any 3.x release from 3.12 on. `terraform init` records the exact version in `.terraform.lock.hcl`. Commit that file in every environment folder, so dev, QA, and production all run the same provider version. To upgrade, change `version` if needed, run `terraform init -upgrade` in `envs/dev`, and copy the updated lock file into the other environment folders.

The module declares the same variables again in its own `variables.tf`, because a module only sees the values passed into it.

## Where state is stored

Terraform saves state every time you apply. Without a backend, it writes state to a `terraform.tfstate` file in the environment's folder. That's fine for trying the workflow out, but the file exists only on your machine. If it's lost, Terraform no longer knows which resources it manages.

Never commit state files. State can hold sensitive values, and a committed copy goes out of date as soon as anyone applies. The `.gitignore` in [What not to commit](#what-not-to-commit) excludes them.

For real use, add a backend to each environment's `main.tf` so state is kept in shared storage outside the repository. For example, with an S3 bucket that already exists:

```hcl theme={null}
# envs/dev/main.tf
terraform {
  # ...
  backend "s3" {
    bucket       = "example-terraform-state"
    key          = "authsignal/dev.tfstate"
    region       = "us-east-1"
    encrypt      = true # encrypts the state at rest
    use_lockfile = true # stops two runs changing the state at once
  }
}
```

Give each environment a different `key`, so each tenant has its own state. Run `terraform init` again after adding or changing a backend. HashiCorp's [backend documentation](https://developer.hashicorp.com/terraform/language/backend) covers S3, HCP Terraform, and the other options.

Turn on versioning for the bucket, so you can restore an earlier copy of the state if it's lost or corrupted. Keep the bucket's public access blocked.

Developers only need the dev state. Give only the pipeline access to the QA and production state, including their `.tflock` lock files. With S3, that means only the pipeline's IAM role can read and write them. Keeping QA and production state in a separate bucket from dev makes this easier than setting permissions on individual files.

[HCP Terraform](https://developer.hashicorp.com/terraform/cloud-docs) is another option. It keeps state and also runs the plans. Use a `cloud` block in place of `backend`:

```hcl theme={null}
# envs/dev/main.tf
terraform {
  # ...
  cloud {
    organization = "your-org"
    workspaces { name = "authsignal-dev" }
  }
}
```

Give each environment its own workspace. In the dev workspace, set the [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) to **Local**, so plans in `envs/dev` run on your machine with the environment variables from [Connecting to a tenant](#connecting-to-a-tenant). With the default, **Remote**, plans run in HCP Terraform and don't see them. QA and production stay on Remote, as in [Running QA and production](#running-qa-and-production).

## Connecting to a tenant

The provider reads three environment variables. On your machine, set them for the dev tenant only. They last until you close the terminal.

```bash theme={null}
export AUTHSIGNAL_HOST="https://api.authsignal.com/v1/management"
export AUTHSIGNAL_TENANT_ID="<dev tenant ID>"
read -rs AUTHSIGNAL_API_SECRET && export AUTHSIGNAL_API_SECRET
```

* `AUTHSIGNAL_HOST` is the [Management API URL](/api-reference/management-api/overview) for your tenant's region. It must end in `/v1/management`.
* The tenant ID and Management API secret key are on the [API keys page](https://portal.authsignal.com/organisations/tenants/api) in the Authsignal Portal, under **Settings → API keys**.
* `read -rs` waits for you to paste the secret key and press Enter. Nothing shows as you paste it, and the key stays out of your shell history. Keep `read` and `export` on one line. On separate lines, pasting the whole block can make `read` take the `export` line as the key.
* Never commit the secret.

The secret key decides which tenant Terraform talks to, whichever folder you're in. On your machine, only run Terraform in `envs/dev`.

## Running QA and production

Run Terraform for QA and production only from a CI pipeline, such as [GitHub Actions](https://docs.github.com/en/actions). That keeps the QA and production secret keys off developer machines. Anyone who has them can change those tenants directly. The pipeline runs these commands in the environment's folder:

```bash theme={null}
cd envs/qa   # or envs/production
terraform init
terraform plan -out=tfplan
terraform apply tfplan
```

However you build the pipeline, it needs to:

* Keep each environment's host, tenant ID, and secret key in your CI system's secrets.
* Make the QA and production secrets available only to runs from your main branch, and protect main so every change needs an approved pull request. In GitHub Actions, put the secrets in a `qa` or `production` environment that only allows deployments from main.
* Never run a QA or production plan on a pull request or any other branch.
* Pause after the plan, and apply only once someone else has read the plan and approved it.
* Reach the [state backend](#where-state-is-stored).

Terraform code can run other programs during a plan, and those programs can read the secret key. A plan of code nobody has reviewed can send the key anywhere, so approving only the apply doesn't protect it. Anyone who can change the pipeline, or run their own code with a QA or production key in any other way, can change that tenant.

A change to `envs/production`, the module, or the pipeline's own files changes production once it's applied, so ask for review from the people who own production.

HashiCorp's [Running Terraform in automation](https://developer.hashicorp.com/terraform/tutorials/automation/automate-terraform) covers how to run Terraform this way. For secrets, approvals, and branch rules, see your CI system's documentation, such as [GitHub Actions environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments).

If you can, run QA and production as two stages of one pipeline, with production waiting for its own approval. Production then gets the same commit that was applied to QA. Separate pipelines each apply whatever is on main when they run, which can include changes QA hasn't had yet.

If the plan and the apply run as separate jobs, the pipeline passes `tfplan` between them. It holds the same values as state, so keep the artifact for as short a time as your CI system allows and limit who can download it.

If you use HCP Terraform, the workspace replaces the pipeline. Set the three `AUTHSIGNAL_` values as [environment variables](https://developer.hashicorp.com/terraform/cloud-docs/variables/managing-variables#workspace-specific-variables) in the QA and production workspaces, and mark the secret key as sensitive. Turn [Auto-apply](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#auto-apply) off and each run waits for approval.

<Warning>
  Anyone who can [queue a plan](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/permissions/workspace) in an HCP Terraform workspace can run their own code with the secret key stored in that workspace, from their own machine or branch. If the workspace is connected to your repository, HCP Terraform also plans every pull request by default, with the secret key stored in the workspace. Turn off [automatic speculative plans](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings/vcs#automatic-speculative-plans) in the QA and production workspaces.
</Warning>

## Reading a plan

`terraform plan` lists each resource it would touch, marked with a symbol:

* `+` means it's created.
* `~` means it's updated in place.
* `-` means it's destroyed.
* `-/+` means it's destroyed and created again.

Resources being imported are marked `will be imported`. For each change, the left side is the current value in the tenant and the right side is the value from the code:

```text theme={null}
~ primary_color = "#2563EB" -> "#1D4ED8"
```

The last line sums it up, such as `Plan: 0 to add, 1 to change, 0 to destroy.` `No changes` means the tenant already matches the code.

These guides save the plan with `terraform plan -out=tfplan`, then run `terraform apply tfplan`. That applies exactly the saved plan without asking you to confirm, so read the plan first. Running `terraform apply` on its own makes a new plan and asks you to type `yes`.

## What not to commit

Add this to the repository's `.gitignore`.

```text theme={null}
.terraform/
*.tfstate
*.tfstate.*
# saved plans hold the same values as state
tfplan
*.tfplan
# crash logs can contain sensitive values
crash.log
crash.*.log
# local overrides and CLI settings, which can hold tokens
override.tf
override.tf.json
*_override.tf
*_override.tf.json
.terraformrc
terraform.rc
# temporary folder used to generate code
scratch/
```

Don't ignore `.terraform.lock.hcl`. Commit it, as in [Environment files](#environment-files).


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