diff --git a/README.md b/README.md
index e632b6c8..cbec7099 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
-# Terraform Provider Coder
+# terraform-provider-coder
 
-> This works with a closed-alpha of [Coder](https://coder.com). For access, contact [support@coder.com](mailto:support@coder.com).
+See [Coder](https://github.com/coder/coder).
diff --git a/docs/data-sources/parameter.md b/docs/data-sources/parameter.md
new file mode 100644
index 00000000..628a4740
--- /dev/null
+++ b/docs/data-sources/parameter.md
@@ -0,0 +1,60 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coder_parameter Data Source - terraform-provider-coder"
+subcategory: ""
+description: |-
+  Use this data source to configure editable options for workspaces.
+---
+
+# coder_parameter (Data Source)
+
+Use this data source to configure editable options for workspaces.
+
+
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `name` (String) The name of the parameter as it will appear in the interface. If this is changed, developers will be re-prompted for a new value.
+
+### Optional
+
+- `default` (String) A default value for the parameter.
+- `description` (String) Describe what this parameter does.
+- `icon` (String) A URL to an icon that will display in the dashboard. View built-in icons here: https://github.com/coder/coder/tree/main/site/static/icon. Use a built-in icon with `data.coder_workspace.me.access_url + "/icon/<path>"`.
+- `mutable` (Boolean) Whether this value can be changed after workspace creation. This can be destructive for values like region, so use with caution!
+- `option` (Block List, Max: 64) Each "option" block defines a value for a user to select from. (see [below for nested schema](#nestedblock--option))
+- `type` (String) The type of this parameter. Must be one of: "number", "string", or "bool".
+- `validation` (Block List, Max: 1) Validate the input of a parameter. (see [below for nested schema](#nestedblock--validation))
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+- `value` (String) The output value of the parameter.
+
+<a id="nestedblock--option"></a>
+### Nested Schema for `option`
+
+Required:
+
+- `name` (String) The display name of this value in the UI.
+- `value` (String) The value of this option set on the parameter if selected.
+
+Optional:
+
+- `description` (String) Describe what selecting this value does.
+- `icon` (String) A URL to an icon that will display in the dashboard. View built-in icons here: https://github.com/coder/coder/tree/main/site/static/icon. Use a built-in icon with `data.coder_workspace.me.access_url + "/icon/<path>"`.
+
+
+<a id="nestedblock--validation"></a>
+### Nested Schema for `validation`
+
+Optional:
+
+- `max` (Number) The maximum of a number parameter.
+- `min` (Number) The minimum of a number parameter.
+- `regex` (String) A regex for the input parameter to match against.
+
+
diff --git a/examples/resources/coder_parameter/resource.tf b/examples/resources/coder_parameter/resource.tf
new file mode 100644
index 00000000..21fb5e4b
--- /dev/null
+++ b/examples/resources/coder_parameter/resource.tf
@@ -0,0 +1,46 @@
+data "coder_parameter" "example" {
+    display_name = "Region"
+    description = "Specify a region to place your workspace."
+    immutable = true
+    type = "string"
+    option {
+        value = "us-central1-a"
+        label = "US Central"
+        icon = "/icon/usa.svg"
+    }
+    option {
+        value = "asia-central1-a"
+        label = "Asia"
+        icon = "/icon/asia.svg"
+    }
+}
+
+data "coder_parameter" "ami" {
+    display_name = "Machine Image"
+    option {
+        value = "ami-xxxxxxxx"
+        label = "Ubuntu"
+        icon = "/icon/ubuntu.svg"
+    }
+}
+
+data "coder_parameter" "image" {
+    display_name = "Docker Image"
+    icon = "/icon/docker.svg"
+    type = "bool"
+}
+
+data "coder_parameter" "cores" {
+    display_name = "CPU Cores"
+    icon = "/icon/"
+}
+
+data "coder_parameter" "disk_size" {
+    display_name = "Disk Size"
+    type = "number"
+    validation {
+        # This can apply to number and string types.
+        min = 0
+        max = 10
+    }
+}
diff --git a/provider/parameter.go b/provider/parameter.go
new file mode 100644
index 00000000..5858a96a
--- /dev/null
+++ b/provider/parameter.go
@@ -0,0 +1,308 @@
+package provider
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"os"
+	"regexp"
+	"strconv"
+
+	"github.com/google/uuid"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
+	"github.com/mitchellh/mapstructure"
+)
+
+type Option struct {
+	Name        string
+	Description string
+	Value       string
+	Icon        string
+}
+
+type Validation struct {
+	Min   int
+	Max   int
+	Regex string
+}
+
+type Parameter struct {
+	Value       string
+	Name        string
+	Description string
+	Type        string
+	Mutable     bool
+	Default     string
+	Icon        string
+	Option      []Option
+	Validation  []Validation
+}
+
+func parameterDataSource() *schema.Resource {
+	return &schema.Resource{
+		Description: "Use this data source to configure editable options for workspaces.",
+		ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
+			rd.SetId(uuid.NewString())
+
+			var parameter Parameter
+			err := mapstructure.Decode(struct {
+				Value       interface{}
+				Name        interface{}
+				Description interface{}
+				Type        interface{}
+				Mutable     interface{}
+				Default     interface{}
+				Icon        interface{}
+				Option      interface{}
+				Validation  interface{}
+			}{
+				Value:       rd.Get("value"),
+				Name:        rd.Get("name"),
+				Description: rd.Get("description"),
+				Type:        rd.Get("type"),
+				Mutable:     rd.Get("mutable"),
+				Default:     rd.Get("default"),
+				Icon:        rd.Get("icon"),
+				Option:      rd.Get("option"),
+				Validation:  rd.Get("validation"),
+			}, &parameter)
+			if err != nil {
+				return diag.Errorf("decode parameter: %s", err)
+			}
+			var value string
+			if parameter.Default != "" {
+				err := valueIsType(parameter.Type, parameter.Default)
+				if err != nil {
+					return err
+				}
+				value = parameter.Default
+			}
+			envValue, ok := os.LookupEnv(fmt.Sprintf("CODER_PARAMETER_%s", parameter.Name))
+			if ok {
+				value = envValue
+			}
+			rd.Set("value", value)
+
+			if len(parameter.Validation) == 1 {
+				validation := &parameter.Validation[0]
+				err = validation.Valid(parameter.Type, value)
+				if err != nil {
+					return diag.FromErr(err)
+				}
+			}
+
+			if len(parameter.Option) > 0 {
+				names := map[string]interface{}{}
+				values := map[string]interface{}{}
+				for _, option := range parameter.Option {
+					_, exists := names[option.Name]
+					if exists {
+						return diag.Errorf("multiple options cannot have the same name %q", option.Name)
+					}
+					_, exists = values[option.Value]
+					if exists {
+						return diag.Errorf("multiple options cannot have the same value %q", option.Value)
+					}
+					err := valueIsType(parameter.Type, option.Value)
+					if err != nil {
+						return err
+					}
+					values[option.Value] = nil
+					names[option.Name] = nil
+				}
+			}
+
+			return nil
+		},
+		Schema: map[string]*schema.Schema{
+			"value": {
+				Type:        schema.TypeString,
+				Computed:    true,
+				Description: "The output value of the parameter.",
+			},
+			"name": {
+				Type:        schema.TypeString,
+				Required:    true,
+				Description: "The name of the parameter as it will appear in the interface. If this is changed, developers will be re-prompted for a new value.",
+			},
+			"description": {
+				Type:        schema.TypeString,
+				Optional:    true,
+				Description: "Describe what this parameter does.",
+			},
+			"type": {
+				Type:         schema.TypeString,
+				Default:      "string",
+				Optional:     true,
+				ValidateFunc: validation.StringInSlice([]string{"number", "string", "bool"}, false),
+				Description:  `The type of this parameter. Must be one of: "number", "string", or "bool".`,
+			},
+			"mutable": {
+				Type:        schema.TypeBool,
+				Optional:    true,
+				Default:     false,
+				Description: "Whether this value can be changed after workspace creation. This can be destructive for values like region, so use with caution!",
+			},
+			"default": {
+				Type:         schema.TypeString,
+				Optional:     true,
+				Description:  "A default value for the parameter.",
+				ExactlyOneOf: []string{"option"},
+			},
+			"icon": {
+				Type: schema.TypeString,
+				Description: "A URL to an icon that will display in the dashboard. View built-in " +
+					"icons here: https://github.com/coder/coder/tree/main/site/static/icon. Use a " +
+					"built-in icon with `data.coder_workspace.me.access_url + \"/icon/<path>\"`.",
+				ForceNew: true,
+				Optional: true,
+				ValidateFunc: func(i interface{}, s string) ([]string, []error) {
+					_, err := url.Parse(s)
+					if err != nil {
+						return nil, []error{err}
+					}
+					return nil, nil
+				},
+			},
+			"option": {
+				Type:          schema.TypeList,
+				Description:   "Each \"option\" block defines a value for a user to select from.",
+				ForceNew:      true,
+				Optional:      true,
+				MaxItems:      64,
+				ConflictsWith: []string{"validation"},
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"name": {
+							Type:        schema.TypeString,
+							Description: "The display name of this value in the UI.",
+							ForceNew:    true,
+							Required:    true,
+						},
+						"description": {
+							Type:        schema.TypeString,
+							Description: "Describe what selecting this value does.",
+							ForceNew:    true,
+							Optional:    true,
+						},
+						"value": {
+							Type:        schema.TypeString,
+							Description: "The value of this option set on the parameter if selected.",
+							ForceNew:    true,
+							Required:    true,
+						},
+						"icon": {
+							Type: schema.TypeString,
+							Description: "A URL to an icon that will display in the dashboard. View built-in " +
+								"icons here: https://github.com/coder/coder/tree/main/site/static/icon. Use a " +
+								"built-in icon with `data.coder_workspace.me.access_url + \"/icon/<path>\"`.",
+							ForceNew: true,
+							Optional: true,
+							ValidateFunc: func(i interface{}, s string) ([]string, []error) {
+								_, err := url.Parse(s)
+								if err != nil {
+									return nil, []error{err}
+								}
+								return nil, nil
+							},
+						},
+					},
+				},
+			},
+			"validation": {
+				Type:          schema.TypeList,
+				MaxItems:      1,
+				Optional:      true,
+				Description:   "Validate the input of a parameter.",
+				ConflictsWith: []string{"option"},
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"min": {
+							Type:         schema.TypeInt,
+							Optional:     true,
+							Default:      0,
+							Description:  "The minimum of a number parameter.",
+							RequiredWith: []string{"validation.0.max"},
+						},
+						"max": {
+							Type:         schema.TypeInt,
+							Optional:     true,
+							Description:  "The maximum of a number parameter.",
+							RequiredWith: []string{"validation.0.min"},
+						},
+						"regex": {
+							Type:          schema.TypeString,
+							ConflictsWith: []string{"validation.0.min", "validation.0.max"},
+							Description:   "A regex for the input parameter to match against.",
+							Optional:      true,
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func valueIsType(typ, value string) diag.Diagnostics {
+	switch typ {
+	case "number":
+		_, err := strconv.ParseFloat(value, 64)
+		if err != nil {
+			return diag.Errorf("%q is not a number", value)
+		}
+	case "bool":
+		_, err := strconv.ParseBool(value)
+		if err != nil {
+			return diag.Errorf("%q is not a bool", value)
+		}
+	case "string":
+		// Anything is a string!
+	default:
+		return diag.Errorf("invalid type %q", typ)
+	}
+	return nil
+}
+
+func (v *Validation) Valid(typ, value string) error {
+	if typ != "number" {
+		if v.Min != 0 {
+			return fmt.Errorf("a min cannot be specified for a %s type", typ)
+		}
+		if v.Max != 0 {
+			return fmt.Errorf("a max cannot be specified for a %s type", typ)
+		}
+	}
+	if typ != "string" && v.Regex != "" {
+		return fmt.Errorf("a regex cannot be specified for a %s type", typ)
+	}
+	switch typ {
+	case "bool":
+		return nil
+	case "string":
+		if v.Regex == "" {
+			return nil
+		}
+		regex, err := regexp.Compile(v.Regex)
+		if err != nil {
+			return fmt.Errorf("compile regex %q: %s", regex, err)
+		}
+		matched := regex.MatchString(value)
+		if !matched {
+			return fmt.Errorf("value %q does not match %q", value, regex)
+		}
+	case "number":
+		num, err := strconv.Atoi(value)
+		if err != nil {
+			return fmt.Errorf("parse value %s as int: %s", value, err)
+		}
+		if num < v.Min {
+			return fmt.Errorf("provided value %d is less than the minimum %d", num, v.Min)
+		}
+		if num > v.Max {
+			return fmt.Errorf("provided value %d is more than the maximum %d", num, v.Max)
+		}
+	}
+	return nil
+}
diff --git a/provider/parameter_test.go b/provider/parameter_test.go
new file mode 100644
index 00000000..874dbbc4
--- /dev/null
+++ b/provider/parameter_test.go
@@ -0,0 +1,314 @@
+package provider_test
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/coder/terraform-provider-coder/provider"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParameter(t *testing.T) {
+	t.Parallel()
+	for _, tc := range []struct {
+		Name        string
+		Config      string
+		ExpectError *regexp.Regexp
+		Check       func(state *terraform.ResourceState)
+	}{{
+		Name: "FieldsExist",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "string"
+	description = "Some option!"
+	mutable = true
+	icon = "/icon/region.svg"
+	option {
+		name = "US Central"
+		value = "us-central1-a"
+		icon = "/icon/central.svg"
+		description = "Select for central!"
+	}
+	option {
+		name = "US East"
+		value = "us-east1-a"
+		icon = "/icon/east.svg"
+		description = "Select for east!"
+	}
+}
+`,
+		Check: func(state *terraform.ResourceState) {
+			attrs := state.Primary.Attributes
+			for key, value := range map[string]interface{}{
+				"name":                 "Region",
+				"type":                 "string",
+				"description":          "Some option!",
+				"mutable":              "true",
+				"icon":                 "/icon/region.svg",
+				"option.0.name":        "US Central",
+				"option.0.value":       "us-central1-a",
+				"option.0.icon":        "/icon/central.svg",
+				"option.0.description": "Select for central!",
+				"option.1.name":        "US East",
+				"option.1.value":       "us-east1-a",
+				"option.1.icon":        "/icon/east.svg",
+				"option.1.description": "Select for east!",
+			} {
+				require.Equal(t, value, attrs[key])
+			}
+		},
+	}, {
+		Name: "ValidationWithOptions",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "number"
+	option {
+		name = "1"
+		value = "1"
+	}
+	validation {
+		regex = "1"
+	}
+}
+`,
+		ExpectError: regexp.MustCompile("conflicts with option"),
+	}, {
+		Name: "NumberValidation",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "number"
+	default = 2
+	validation {
+		min = 1
+		max = 5
+	}
+}
+`,
+		Check: func(state *terraform.ResourceState) {
+
+		},
+	}, {
+		Name: "DefaultNotNumber",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "number"
+	default = true
+}
+`,
+		ExpectError: regexp.MustCompile("is not a number"),
+	}, {
+		Name: "DefaultNotBool",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "bool"
+	default = 5
+}
+`,
+		ExpectError: regexp.MustCompile("is not a bool"),
+	}, {
+		Name: "OptionNotBool",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "bool"
+	option {
+		value = 1
+		name = 1
+	}
+	option {
+		value = 2
+		name = 2
+	}
+}`,
+		ExpectError: regexp.MustCompile("\"2\" is not a bool"),
+	}, {
+		Name: "MultipleOptions",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "string"
+	option {
+		name = "1"
+		value = "1"
+		icon = "/icon/code.svg"
+		description = "Something!"
+	}
+	option {
+		name = "2"
+		value = "2"
+	}
+}
+`,
+		Check: func(state *terraform.ResourceState) {
+			for key, expected := range map[string]string{
+				"name":                 "Region",
+				"option.#":             "2",
+				"option.0.name":        "1",
+				"option.0.value":       "1",
+				"option.0.icon":        "/icon/code.svg",
+				"option.0.description": "Something!",
+			} {
+				require.Equal(t, expected, state.Primary.Attributes[key])
+			}
+		},
+	}, {
+		Name: "DefaultWithOption",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	default = "hi"
+	option {
+		name = "1"
+		value = "1"
+	}
+	option {
+		name = "2"
+		value = "2"
+	}
+}
+`,
+		ExpectError: regexp.MustCompile("Invalid combination of arguments"),
+	}, {
+		Name: "SingleOption",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	option {
+		name = "1"
+		value = "1"
+	}
+}
+`,
+	}, {
+		Name: "DuplicateOptionName",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "string"
+	option {
+		name = "1"
+		value = "1"
+	}
+	option {
+		name = "1"
+		value = "2"
+	}
+}
+`,
+		ExpectError: regexp.MustCompile("cannot have the same name"),
+	}, {
+		Name: "DuplicateOptionValue",
+		Config: `
+data "coder_parameter" "region" {
+	name = "Region"
+	type = "string"
+	option {
+		name = "1"
+		value = "1"
+	}
+	option {
+		name = "2"
+		value = "1"
+	}
+}
+`,
+		ExpectError: regexp.MustCompile("cannot have the same value"),
+	}} {
+		tc := tc
+		t.Run(tc.Name, func(t *testing.T) {
+			t.Parallel()
+			resource.Test(t, resource.TestCase{
+				Providers: map[string]*schema.Provider{
+					"coder": provider.New(),
+				},
+				IsUnitTest: true,
+				Steps: []resource.TestStep{{
+					Config:      tc.Config,
+					ExpectError: tc.ExpectError,
+					Check: func(state *terraform.State) error {
+						require.Len(t, state.Modules, 1)
+						require.Len(t, state.Modules[0].Resources, 1)
+						param := state.Modules[0].Resources["data.coder_parameter.region"]
+						require.NotNil(t, param)
+						t.Logf("parameter attributes: %#v", param.Primary.Attributes)
+						if tc.Check != nil {
+							tc.Check(param)
+						}
+						return nil
+					},
+				}},
+			})
+		})
+	}
+}
+
+func TestValueValidatesType(t *testing.T) {
+	t.Parallel()
+	for _, tc := range []struct {
+		Name,
+		Type,
+		Value,
+		Regex string
+		Min,
+		Max int
+		Error *regexp.Regexp
+	}{{
+		Name:  "StringWithMin",
+		Type:  "string",
+		Min:   1,
+		Error: regexp.MustCompile("cannot be specified"),
+	}, {
+		Name:  "StringWithMax",
+		Type:  "string",
+		Max:   1,
+		Error: regexp.MustCompile("cannot be specified"),
+	}, {
+		Name:  "NonStringWithRegex",
+		Type:  "number",
+		Regex: "banana",
+		Error: regexp.MustCompile("a regex cannot be specified"),
+	}, {
+		Name:  "Bool",
+		Type:  "bool",
+		Value: "true",
+	}, {
+		Name:  "InvalidNumber",
+		Type:  "number",
+		Value: "hi",
+		Error: regexp.MustCompile("parse value hi as int"),
+	}, {
+		Name:  "NumberBelowMin",
+		Type:  "number",
+		Value: "0",
+		Min:   1,
+		Error: regexp.MustCompile("is less than the minimum"),
+	}, {
+		Name:  "NumberAboveMax",
+		Type:  "number",
+		Value: "1",
+		Max:   0,
+		Error: regexp.MustCompile("is more than the maximum"),
+	}} {
+		tc := tc
+		t.Run(tc.Name, func(t *testing.T) {
+			t.Parallel()
+			v := &provider.Validation{
+				Min:   tc.Min,
+				Max:   tc.Max,
+				Regex: tc.Regex,
+			}
+			err := v.Valid(tc.Type, tc.Value)
+			if tc.Error != nil {
+				require.True(t, tc.Error.MatchString(err.Error()))
+			}
+		})
+	}
+}
diff --git a/provider/provider.go b/provider/provider.go
index adc0635a..2032cec3 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -63,6 +63,7 @@ func New() *schema.Provider {
 		DataSourcesMap: map[string]*schema.Resource{
 			"coder_workspace":   workspaceDataSource(),
 			"coder_provisioner": provisionerDataSource(),
+			"coder_parameter":   parameterDataSource(),
 		},
 		ResourcesMap: map[string]*schema.Resource{
 			"coder_agent":          agentResource(),