diff --git a/Readme.md b/Readme.md
index 5dd08be..88dd723 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1 +1,9 @@
 # Miniauth.
+
+
+## Config
+
+| Name | Description | Default |
+|------|-------------|---------|
+| USERSTORE_SQLITE_PATH | Path to the Sqlite Database | `none` |
+| PORT | Port for the Webserver | 8080 |
diff --git a/go.mod b/go.mod
index d9f387d..a0b5594 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/go-passwd/validator v0.0.0-20180902184246-0b4c967e436b
 	github.com/google/uuid v1.6.0
 	github.com/rs/zerolog v1.33.0
+	github.com/sethvargo/go-envconfig v1.1.1
 	github.com/stretchr/testify v1.9.0
 	github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d
 	golang.org/x/crypto v0.23.0
diff --git a/go.sum b/go.sum
index eed30bf..b4c7bc5 100644
--- a/go.sum
+++ b/go.sum
@@ -37,8 +37,8 @@ github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaC
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
 github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
@@ -75,6 +75,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
 github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
+github.com/sethvargo/go-envconfig v1.1.1 h1:JDu8Q9baIzJf47NPkzhIB6aLYL0vQ+pPypoYrejS9QY=
+github.com/sethvargo/go-envconfig v1.1.1/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
diff --git a/main.go b/main.go
index 962d9ea..51373e7 100644
--- a/main.go
+++ b/main.go
@@ -1,19 +1,39 @@
 package main
 
 import (
+	"context"
 	"embed"
+	"fmt"
 	"html/template"
 
+	"git.keks.cloud/kekskurse/miniauth/pkg/userstore"
 	"git.keks.cloud/kekskurse/miniauth/pkg/web"
 	"github.com/gin-gonic/gin"
+	"github.com/rs/zerolog/log"
+	"github.com/sethvargo/go-envconfig"
 )
 
 //go:embed templates/*
 var templatesFS embed.FS
 
 func main() {
+	cnf := config()
 	router := setupRouter()
-	router.Run(":8080")
+	router.Run(fmt.Sprintf(":%v", cnf.Port))
+}
+
+type gloableConfig struct {
+	UserStoreConfig userstore.Config `env:", prefix=USERSTORE_"`
+	Port            int              `env:"PORT, default=8080"`
+}
+
+func config() gloableConfig {
+	var c gloableConfig
+	err := envconfig.Process(context.Background(), &c)
+	if err != nil {
+		log.Fatal().Err(err).Msg("cant parse config")
+	}
+	return c
 }
 
 func loadTemplates() *template.Template {
diff --git a/pkg/userstore/store.go b/pkg/userstore/store.go
index 033726b..fbfc2a4 100644
--- a/pkg/userstore/store.go
+++ b/pkg/userstore/store.go
@@ -20,7 +20,7 @@ var schema string
 
 type Config struct {
 	SQLite struct {
-		Path string
+		Path string `env:"SQLITE_PATH"`
 	}
 	Logger zerolog.Logger
 }
diff --git a/vendor/github.com/rs/zerolog/log/log.go b/vendor/github.com/rs/zerolog/log/log.go
new file mode 100644
index 0000000..a96ec50
--- /dev/null
+++ b/vendor/github.com/rs/zerolog/log/log.go
@@ -0,0 +1,131 @@
+// Package log provides a global logger for zerolog.
+package log
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/rs/zerolog"
+)
+
+// Logger is the global logger.
+var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
+
+// Output duplicates the global logger and sets w as its output.
+func Output(w io.Writer) zerolog.Logger {
+	return Logger.Output(w)
+}
+
+// With creates a child logger with the field added to its context.
+func With() zerolog.Context {
+	return Logger.With()
+}
+
+// Level creates a child logger with the minimum accepted level set to level.
+func Level(level zerolog.Level) zerolog.Logger {
+	return Logger.Level(level)
+}
+
+// Sample returns a logger with the s sampler.
+func Sample(s zerolog.Sampler) zerolog.Logger {
+	return Logger.Sample(s)
+}
+
+// Hook returns a logger with the h Hook.
+func Hook(h zerolog.Hook) zerolog.Logger {
+	return Logger.Hook(h)
+}
+
+// Err starts a new message with error level with err as a field if not nil or
+// with info level if err is nil.
+//
+// You must call Msg on the returned event in order to send the event.
+func Err(err error) *zerolog.Event {
+	return Logger.Err(err)
+}
+
+// Trace starts a new message with trace level.
+//
+// You must call Msg on the returned event in order to send the event.
+func Trace() *zerolog.Event {
+	return Logger.Trace()
+}
+
+// Debug starts a new message with debug level.
+//
+// You must call Msg on the returned event in order to send the event.
+func Debug() *zerolog.Event {
+	return Logger.Debug()
+}
+
+// Info starts a new message with info level.
+//
+// You must call Msg on the returned event in order to send the event.
+func Info() *zerolog.Event {
+	return Logger.Info()
+}
+
+// Warn starts a new message with warn level.
+//
+// You must call Msg on the returned event in order to send the event.
+func Warn() *zerolog.Event {
+	return Logger.Warn()
+}
+
+// Error starts a new message with error level.
+//
+// You must call Msg on the returned event in order to send the event.
+func Error() *zerolog.Event {
+	return Logger.Error()
+}
+
+// Fatal starts a new message with fatal level. The os.Exit(1) function
+// is called by the Msg method.
+//
+// You must call Msg on the returned event in order to send the event.
+func Fatal() *zerolog.Event {
+	return Logger.Fatal()
+}
+
+// Panic starts a new message with panic level. The message is also sent
+// to the panic function.
+//
+// You must call Msg on the returned event in order to send the event.
+func Panic() *zerolog.Event {
+	return Logger.Panic()
+}
+
+// WithLevel starts a new message with level.
+//
+// You must call Msg on the returned event in order to send the event.
+func WithLevel(level zerolog.Level) *zerolog.Event {
+	return Logger.WithLevel(level)
+}
+
+// Log starts a new message with no level. Setting zerolog.GlobalLevel to
+// zerolog.Disabled will still disable events produced by this method.
+//
+// You must call Msg on the returned event in order to send the event.
+func Log() *zerolog.Event {
+	return Logger.Log()
+}
+
+// Print sends a log event using debug level and no extra field.
+// Arguments are handled in the manner of fmt.Print.
+func Print(v ...interface{}) {
+	Logger.Debug().CallerSkipFrame(1).Msg(fmt.Sprint(v...))
+}
+
+// Printf sends a log event using debug level and no extra field.
+// Arguments are handled in the manner of fmt.Printf.
+func Printf(format string, v ...interface{}) {
+	Logger.Debug().CallerSkipFrame(1).Msgf(format, v...)
+}
+
+// Ctx returns the Logger associated with the ctx. If no logger
+// is associated, a disabled logger is returned.
+func Ctx(ctx context.Context) *zerolog.Logger {
+	return zerolog.Ctx(ctx)
+}
diff --git a/vendor/github.com/sethvargo/go-envconfig/AUTHORS b/vendor/github.com/sethvargo/go-envconfig/AUTHORS
new file mode 100644
index 0000000..2107f4a
--- /dev/null
+++ b/vendor/github.com/sethvargo/go-envconfig/AUTHORS
@@ -0,0 +1,8 @@
+# This is the list of envconfig authors for copyright purposes.
+#
+# This does not necessarily list everyone who has contributed code, since in
+# some cases, their employer may be the copyright holder. To see the full list
+# of contributors, see the revision history in source control.
+
+Google LLC
+Seth Vargo
diff --git a/vendor/github.com/sethvargo/go-envconfig/LICENSE b/vendor/github.com/sethvargo/go-envconfig/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/vendor/github.com/sethvargo/go-envconfig/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/vendor/github.com/sethvargo/go-envconfig/Makefile b/vendor/github.com/sethvargo/go-envconfig/Makefile
new file mode 100644
index 0000000..b95f7a7
--- /dev/null
+++ b/vendor/github.com/sethvargo/go-envconfig/Makefile
@@ -0,0 +1,22 @@
+# Copyright The envconfig Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+test:
+	@go test \
+		-count=1 \
+		-race \
+		-shuffle=on \
+		-timeout=10m \
+		./...
+.PHONY: test
diff --git a/vendor/github.com/sethvargo/go-envconfig/README.md b/vendor/github.com/sethvargo/go-envconfig/README.md
new file mode 100644
index 0000000..49c2fe1
--- /dev/null
+++ b/vendor/github.com/sethvargo/go-envconfig/README.md
@@ -0,0 +1,306 @@
+# Envconfig
+
+[![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)][godoc]
+
+Envconfig populates struct field values based on environment variables or
+arbitrary lookup functions. It supports pre-setting mutations, which is useful
+for things like converting values to uppercase, trimming whitespace, or looking
+up secrets.
+
+## Usage
+
+Define a struct with fields using the `env` tag:
+
+```go
+type MyConfig struct {
+  Port     string `env:"PORT"`
+  Username string `env:"USERNAME"`
+}
+```
+
+Set some environment variables:
+
+```sh
+export PORT=5555
+export USERNAME=yoyo
+```
+
+Process it using envconfig:
+
+```go
+package main
+
+import (
+  "context"
+  "log"
+
+  "github.com/sethvargo/go-envconfig"
+)
+
+func main() {
+  ctx := context.Background()
+
+  var c MyConfig
+  if err := envconfig.Process(ctx, &c); err != nil {
+    log.Fatal(err)
+  }
+
+  // c.Port = 5555
+  // c.Username = "yoyo"
+}
+```
+
+You can also use nested structs, just remember that any fields you want to
+process must be public:
+
+```go
+type MyConfig struct {
+  Database *DatabaseConfig
+}
+
+type DatabaseConfig struct {
+  Port     string `env:"PORT"`
+  Username string `env:"USERNAME"`
+}
+```
+
+## Configuration
+
+Use the `env` struct tag to define configuration. See the [godoc][] for usage
+examples.
+
+-   `required` - marks a field as required. If a field is required, decoding
+    will error if the environment variable is unset.
+
+    ```go
+    type MyStruct struct {
+      Port string `env:"PORT, required"`
+    }
+    ```
+
+-   `default` - sets the default value for the environment variable is not set.
+    The environment variable must not be set (e.g. `unset PORT`). If the
+    environment variable is the empty string, envconfig considers that a "value"
+    and the default will **not** be used.
+
+    You can also set the default value to the value from another field or a
+    value from a different environment variable.
+
+    ```go
+    type MyStruct struct {
+      Port string `env:"PORT, default=5555"`
+      User string `env:"USER, default=$CURRENT_USER"`
+    }
+    ```
+
+-   `prefix` - sets the prefix to use for looking up environment variable keys
+    on child structs and fields. This is useful for shared configurations:
+
+    ```go
+    type RedisConfig struct {
+      Host string `env:"REDIS_HOST"`
+      User string `env:"REDIS_USER"`
+    }
+
+    type ServerConfig struct {
+      // CacheConfig will process values from $CACHE_REDIS_HOST and
+      // $CACHE_REDIS_USER respectively.
+      CacheConfig *RedisConfig `env:", prefix=CACHE_"`
+
+      // RateLimitConfig will process values from $RATE_LIMIT_REDIS_HOST and
+      // $RATE_LIMIT_REDIS_USER respectively.
+      RateLimitConfig *RedisConfig `env:", prefix=RATE_LIMIT_"`
+    }
+    ```
+
+-   `overwrite` - force overwriting existing non-zero struct values if the
+    environment variable was provided.
+
+    ```go
+    type MyStruct struct {
+      Port string `env:"PORT, overwrite"`
+    }
+    ```
+
+    The rules for overwrite + default are:
+
+    -   If the struct field has the zero value and a default is set:
+
+        -   If no environment variable is specified, the struct field will be
+            populated with the default value.
+
+        -   If an environment variable is specified, the struct field will be
+            populate with the environment variable value.
+
+    -   If the struct field has a non-zero value and a default is set:
+
+        -   If no environment variable is specified, the struct field's existing
+            value will be used (the default is ignored).
+
+        -   If an environment variable is specified, the struct field's existing
+            value will be overwritten with the environment variable value.
+
+-   `delimiter` - choose a custom character to denote individual slice and map
+    entries. The default value is the comma (`,`).
+
+    ```go
+    type MyStruct struct {
+      MyVar []string `env:"MYVAR, delimiter=;"`
+    ```
+
+    ```bash
+    export MYVAR="a;b;c;d" # []string{"a", "b", "c", "d"}
+    ```
+
+-   `separator` - choose a custom character to denote the separation between
+    keys and values in map entries. The default value is the colon (`:`) Define
+    a separator with `separator`:
+
+    ```go
+    type MyStruct struct {
+      MyVar map[string]string `env:"MYVAR, separator=|"`
+    }
+    ```
+
+    ```bash
+    export MYVAR="a|b,c|d" # map[string]string{"a":"b", "c":"d"}
+    ```
+
+-   `noinit` - do not initialize struct fields unless environment variables were
+    provided. The default behavior is to deeply initialize all fields to their
+    default (zero) value.
+
+    ```go
+    type MyStruct struct {
+      MyVar *url.URL `env:"MYVAR, noinit"`
+    }
+    ```
+
+-   `decodeunset` - force envconfig to run decoders even on unset environment
+    variable values. The default behavior is to skip running decoders on unset
+    environment variable values.
+
+    ```go
+    type MyStruct struct {
+      MyVar *url.URL `env:"MYVAR, decodeunset"`
+    }
+    ```
+
+
+## Decoding
+
+> [!NOTE]
+>
+> Complex types are only decoded or unmarshalled when the environment variable
+> is defined or a default value is specified.
+
+
+### Durations
+
+In the environment, `time.Duration` values are specified as a parsable Go
+duration:
+
+```go
+type MyStruct struct {
+  MyVar time.Duration `env:"MYVAR"`
+}
+```
+
+```bash
+export MYVAR="10m" # 10 * time.Minute
+```
+
+
+### TextUnmarshaler / BinaryUnmarshaler
+
+Types that implement `TextUnmarshaler` or `BinaryUnmarshaler` are processed as
+such.
+
+
+### json.Unmarshaler
+
+Types that implement `json.Unmarshaler` are processed as such.
+
+
+### gob.Decoder
+
+Types that implement `gob.Decoder` are processed as such.
+
+
+### Slices
+
+Slices are specified as comma-separated values.
+
+```go
+type MyStruct struct {
+  MyVar []string `env:"MYVAR"`
+}
+```
+
+```bash
+export MYVAR="a,b,c,d" # []string{"a", "b", "c", "d"}
+```
+
+Note that byte slices are special cased and interpreted as strings from the
+environment.
+
+
+### Maps
+
+Maps are specified as comma-separated key:value pairs:
+
+```go
+type MyStruct struct {
+  MyVar map[string]string `env:"MYVAR"`
+}
+```
+
+```bash
+export MYVAR="a:b,c:d" # map[string]string{"a":"b", "c":"d"}
+```
+
+
+### Structs
+
+Envconfig walks the entire struct, including nested structs, so deeply-nested
+fields are also supported.
+
+If a nested struct is a pointer type, it will automatically be instantianted to
+the non-nil value. To change this behavior, see
+[Initialization](#Initialization).
+
+
+### Custom Decoders
+
+You can also define your own decoders. See the [godoc][godoc] for more
+information.
+
+
+## Testing
+
+Relying on the environment in tests can be troublesome because environment
+variables are global, which makes it difficult to parallelize the tests.
+Envconfig supports extracting data from anything that returns a value:
+
+```go
+lookuper := envconfig.MapLookuper(map[string]string{
+  "FOO": "bar",
+  "ZIP": "zap",
+})
+
+var config Config
+envconfig.ProcessWith(ctx, &envconfig.Config{
+  Target:   &config,
+  Lookuper: lookuper,
+})
+```
+
+Now you can parallelize all your tests by providing a map for the lookup
+function. In fact, that's how the tests in this repo work, so check there for an
+example.
+
+You can also combine multiple lookupers with `MultiLookuper`. See the GoDoc for
+more information and examples.
+
+
+[godoc]: https://pkg.go.dev/mod/github.com/sethvargo/go-envconfig
diff --git a/vendor/github.com/sethvargo/go-envconfig/decoding.go b/vendor/github.com/sethvargo/go-envconfig/decoding.go
new file mode 100644
index 0000000..3d1cd28
--- /dev/null
+++ b/vendor/github.com/sethvargo/go-envconfig/decoding.go
@@ -0,0 +1,57 @@
+// Copyright The envconfig Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package envconfig
+
+import (
+	"encoding/base64"
+	"encoding/hex"
+	"strings"
+)
+
+// Base64Bytes is a slice of bytes where the information is base64-encoded in
+// the environment variable.
+type Base64Bytes []byte
+
+// EnvDecode implements env.Decoder.
+func (b *Base64Bytes) EnvDecode(val string) error {
+	val = strings.ReplaceAll(val, "+", "-")
+	val = strings.ReplaceAll(val, "/", "_")
+	val = strings.TrimRight(val, "=")
+
+	var err error
+	*b, err = base64.RawURLEncoding.DecodeString(val)
+	return err
+}
+
+// Bytes returns the underlying bytes.
+func (b Base64Bytes) Bytes() []byte {
+	return []byte(b)
+}
+
+// HexBytes is a slice of bytes where the information is hex-encoded in the
+// environment variable.
+type HexBytes []byte
+
+// EnvDecode implements env.Decoder.
+func (b *HexBytes) EnvDecode(val string) error {
+	var err error
+	*b, err = hex.DecodeString(val)
+	return err
+}
+
+// Bytes returns the underlying bytes.
+func (b HexBytes) Bytes() []byte {
+	return []byte(b)
+}
diff --git a/vendor/github.com/sethvargo/go-envconfig/envconfig.go b/vendor/github.com/sethvargo/go-envconfig/envconfig.go
new file mode 100644
index 0000000..58919aa
--- /dev/null
+++ b/vendor/github.com/sethvargo/go-envconfig/envconfig.go
@@ -0,0 +1,877 @@
+// Copyright The envconfig Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package envconfig populates struct fields based on environment variable
+// values (or anything that responds to "Lookup"). Structs declare their
+// environment dependencies using the "env" tag with the key being the name of
+// the environment variable, case sensitive.
+//
+//	type MyStruct struct {
+//	  A string `env:"A"` // resolves A to $A
+//	  B string `env:"B,required"` // resolves B to $B, errors if $B is unset
+//	  C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
+//
+//	  D string `env:"D,required,default=foo"` // error, cannot be required and default
+//	  E string `env:""` // error, must specify key
+//	}
+//
+// All built-in types are supported except Func and Chan. If you need to define
+// a custom decoder, implement Decoder:
+//
+//	type MyStruct struct {
+//	  field string
+//	}
+//
+//	func (v *MyStruct) EnvDecode(val string) error {
+//	  v.field = fmt.Sprintf("PREFIX-%s", val)
+//	  return nil
+//	}
+//
+// In the environment, slices are specified as comma-separated values:
+//
+//	export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}
+//
+// In the environment, maps are specified as comma-separated key:value pairs:
+//
+//	export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}
+//
+// For more configuration options and examples, see the documentation.
+package envconfig
+
+import (
+	"context"
+	"encoding"
+	"encoding/gob"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+	"unicode"
+)
+
+const (
+	envTag = "env"
+
+	optDecodeUnset = "decodeunset"
+	optDefault     = "default="
+	optDelimiter   = "delimiter="
+	optNoInit      = "noinit"
+	optOverwrite   = "overwrite"
+	optPrefix      = "prefix="
+	optRequired    = "required"
+	optSeparator   = "separator="
+)
+
+// internalError is a custom error type for errors returned by envconfig.
+type internalError string
+
+// Error implements error.
+func (e internalError) Error() string {
+	return string(e)
+}
+
+const (
+	ErrInvalidEnvvarName  = internalError("invalid environment variable name")
+	ErrInvalidMapItem     = internalError("invalid map item")
+	ErrLookuperNil        = internalError("lookuper cannot be nil")
+	ErrMissingKey         = internalError("missing key")
+	ErrMissingRequired    = internalError("missing required value")
+	ErrNoInitNotPtr       = internalError("field must be a pointer to have noinit")
+	ErrNotPtr             = internalError("input must be a pointer")
+	ErrNotStruct          = internalError("input must be a struct")
+	ErrPrefixNotStruct    = internalError("prefix is only valid on struct types")
+	ErrPrivateField       = internalError("cannot parse private fields")
+	ErrRequiredAndDefault = internalError("field cannot be required and have a default value")
+	ErrUnknownOption      = internalError("unknown option")
+)
+
+// Lookuper is an interface that provides a lookup for a string-based key.
+type Lookuper interface {
+	// Lookup searches for the given key and returns the corresponding string
+	// value. If a value is found, it returns the value and true. If a value is
+	// not found, it returns the empty string and false.
+	Lookup(key string) (string, bool)
+}
+
+// osLookuper looks up environment configuration from the local environment.
+type osLookuper struct{}
+
+// Verify implements interface.
+var _ Lookuper = (*osLookuper)(nil)
+
+func (o *osLookuper) Lookup(key string) (string, bool) {
+	return os.LookupEnv(key)
+}
+
+// OsLookuper returns a lookuper that uses the environment ([os.LookupEnv]) to
+// resolve values.
+func OsLookuper() Lookuper {
+	return new(osLookuper)
+}
+
+type mapLookuper map[string]string
+
+var _ Lookuper = (*mapLookuper)(nil)
+
+func (m mapLookuper) Lookup(key string) (string, bool) {
+	v, ok := m[key]
+	return v, ok
+}
+
+// MapLookuper looks up environment configuration from a provided map. This is
+// useful for testing, especially in parallel, since it does not require you to
+// mutate the parent environment (which is stateful).
+func MapLookuper(m map[string]string) Lookuper {
+	return mapLookuper(m)
+}
+
+type multiLookuper struct {
+	ls []Lookuper
+}
+
+var _ Lookuper = (*multiLookuper)(nil)
+
+func (m *multiLookuper) Lookup(key string) (string, bool) {
+	for _, l := range m.ls {
+		if v, ok := l.Lookup(key); ok {
+			return v, true
+		}
+	}
+	return "", false
+}
+
+// PrefixLookuper looks up environment configuration using the specified prefix.
+// This is useful if you want all your variables to start with a particular
+// prefix like "MY_APP_".
+func PrefixLookuper(prefix string, l Lookuper) Lookuper {
+	if typ, ok := l.(*prefixLookuper); ok {
+		return &prefixLookuper{prefix: typ.prefix + prefix, l: typ.l}
+	}
+	return &prefixLookuper{prefix: prefix, l: l}
+}
+
+type prefixLookuper struct {
+	l      Lookuper
+	prefix string
+}
+
+func (p *prefixLookuper) Lookup(key string) (string, bool) {
+	return p.l.Lookup(p.Key(key))
+}
+
+func (p *prefixLookuper) Key(key string) string {
+	return p.prefix + key
+}
+
+func (p *prefixLookuper) Unwrap() Lookuper {
+	l := p.l
+	for v, ok := l.(unwrappableLookuper); ok; {
+		l = v.Unwrap()
+	}
+	return l
+}
+
+// unwrappableLookuper is a lookuper that can return the underlying lookuper.
+type unwrappableLookuper interface {
+	Unwrap() Lookuper
+}
+
+// MultiLookuper wraps a collection of lookupers. It does not combine them, and
+// lookups appear in the order in which they are provided to the initializer.
+func MultiLookuper(lookupers ...Lookuper) Lookuper {
+	return &multiLookuper{ls: lookupers}
+}
+
+// keyedLookuper is an extension to the [Lookuper] interface that returns the
+// underlying key (used by the [PrefixLookuper] or custom implementations).
+type keyedLookuper interface {
+	Key(key string) string
+}
+
+// Decoder is an interface that custom types/fields can implement to control how
+// decoding takes place. For example:
+//
+//	type MyType string
+//
+//	func (mt MyType) EnvDecode(val string) error {
+//	    return "CUSTOM-"+val
+//	}
+type Decoder interface {
+	EnvDecode(val string) error
+}
+
+// options are internal options for decoding.
+type options struct {
+	Default     string
+	Delimiter   string
+	Prefix      string
+	Separator   string
+	NoInit      bool
+	Overwrite   bool
+	DecodeUnset bool
+	Required    bool
+}
+
+// Config represent inputs to the envconfig decoding.
+type Config struct {
+	// Target is the destination structure to decode. This value is required, and
+	// it must be a pointer to a struct.
+	Target any
+
+	// Lookuper is the lookuper implementation to use. If not provided, it
+	// defaults to the OS Lookuper.
+	Lookuper Lookuper
+
+	// DefaultDelimiter is the default value to use for the delimiter in maps and
+	// slices. This can be overridden on a per-field basis, which takes
+	// precedence. The default value is ",".
+	DefaultDelimiter string
+
+	// DefaultSeparator is the default value to use for the separator in maps.
+	// This can be overridden on a per-field basis, which takes precedence. The
+	// default value is ":".
+	DefaultSeparator string
+
+	// DefaultNoInit is the default value for skipping initialization of
+	// unprovided fields. The default value is false (deeply initialize all
+	// fields and nested structs).
+	DefaultNoInit bool
+
+	// DefaultOverwrite is the default value for overwriting an existing value set
+	// on the struct before processing. The default value is false.
+	DefaultOverwrite bool
+
+	// DefaultDecodeUnset is the default value for running decoders even when no
+	// value was given for the environment variable.
+	DefaultDecodeUnset bool
+
+	// DefaultRequired is the default value for marking a field as required. The
+	// default value is false.
+	DefaultRequired bool
+
+	// Mutators is an optional list of mutators to apply to lookups.
+	Mutators []Mutator
+}
+
+// Process decodes the struct using values from environment variables. See
+// [ProcessWith] for a more customizable version.
+//
+// As a special case, if the input for the target is a [*Config], then this
+// function will call [ProcessWith] using the provided config, with any mutation
+// appended.
+func Process(ctx context.Context, i any, mus ...Mutator) error {
+	if v, ok := i.(*Config); ok {
+		v.Mutators = append(v.Mutators, mus...)
+		return ProcessWith(ctx, v)
+	}
+	return ProcessWith(ctx, &Config{
+		Target:   i,
+		Mutators: mus,
+	})
+}
+
+// MustProcess is a helper that calls [Process] and panics if an error is
+// encountered. Unlike [Process], the input value is returned, making it ideal
+// for anonymous initializations:
+//
+//	var env = envconfig.MustProcess(context.Background(), &struct{
+//	  Field string `env:"FIELD,required"`
+//	})
+//
+// This is not recommend for production services, but it can be useful for quick
+// CLIs and scripts that want to take advantage of envconfig's environment
+// parsing at the expense of testability and graceful error handling.
+func MustProcess[T any](ctx context.Context, i T, mus ...Mutator) T {
+	if err := Process(ctx, i, mus...); err != nil {
+		panic(err)
+	}
+	return i
+}
+
+// ProcessWith executes the decoding process using the provided [Config].
+func ProcessWith(ctx context.Context, c *Config) error {
+	if c == nil {
+		c = new(Config)
+	}
+
+	if c.Lookuper == nil {
+		c.Lookuper = OsLookuper()
+	}
+
+	// Deep copy the slice and remove any nil mutators.
+	var mus []Mutator
+	for _, m := range c.Mutators {
+		if m != nil {
+			mus = append(mus, m)
+		}
+	}
+	c.Mutators = mus
+
+	return processWith(ctx, c)
+}
+
+// processWith is a helper that retains configuration from the parent structs.
+func processWith(ctx context.Context, c *Config) error {
+	i := c.Target
+
+	l := c.Lookuper
+	if l == nil {
+		return ErrLookuperNil
+	}
+
+	v := reflect.ValueOf(i)
+	if v.Kind() != reflect.Ptr {
+		return ErrNotPtr
+	}
+
+	e := v.Elem()
+	if e.Kind() != reflect.Struct {
+		return ErrNotStruct
+	}
+
+	t := e.Type()
+
+	structDelimiter := c.DefaultDelimiter
+	if structDelimiter == "" {
+		structDelimiter = ","
+	}
+
+	structNoInit := c.DefaultNoInit
+
+	structSeparator := c.DefaultSeparator
+	if structSeparator == "" {
+		structSeparator = ":"
+	}
+
+	structOverwrite := c.DefaultOverwrite
+	structDecodeUnset := c.DefaultDecodeUnset
+	structRequired := c.DefaultRequired
+
+	mutators := c.Mutators
+
+	for i := 0; i < t.NumField(); i++ {
+		ef := e.Field(i)
+		tf := t.Field(i)
+		tag := tf.Tag.Get(envTag)
+
+		if !ef.CanSet() {
+			if tag != "" {
+				// There's an "env" tag on a private field, we can't alter it, and it's
+				// likely a mistake. Return an error so the user can handle.
+				return fmt.Errorf("%s: %w", tf.Name, ErrPrivateField)
+			}
+
+			// Otherwise continue to the next field.
+			continue
+		}
+
+		// Parse the key and options.
+		key, opts, err := keyAndOpts(tag)
+		if err != nil {
+			return fmt.Errorf("%s: %w", tf.Name, err)
+		}
+
+		// NoInit is only permitted on pointers.
+		if opts.NoInit &&
+			ef.Kind() != reflect.Ptr &&
+			ef.Kind() != reflect.Slice &&
+			ef.Kind() != reflect.Map &&
+			ef.Kind() != reflect.UnsafePointer {
+			return fmt.Errorf("%s: %w", tf.Name, ErrNoInitNotPtr)
+		}
+
+		// Compute defaults from local tags.
+		delimiter := structDelimiter
+		if v := opts.Delimiter; v != "" {
+			delimiter = v
+		}
+		separator := structSeparator
+		if v := opts.Separator; v != "" {
+			separator = v
+		}
+
+		noInit := structNoInit || opts.NoInit
+		overwrite := structOverwrite || opts.Overwrite
+		decodeUnset := structDecodeUnset || opts.DecodeUnset
+		required := structRequired || opts.Required
+
+		isNilStructPtr := false
+		setNilStruct := func(v reflect.Value) {
+			origin := e.Field(i)
+			if isNilStructPtr {
+				empty := reflect.New(origin.Type().Elem()).Interface()
+
+				// If a struct (after traversal) equals to the empty value, it means
+				// nothing was changed in any sub-fields. With the noinit opt, we skip
+				// setting the empty value to the original struct pointer (keep it nil).
+				if !reflect.DeepEqual(v.Interface(), empty) || !noInit {
+					origin.Set(v)
+				}
+			}
+		}
+
+		// Initialize pointer structs.
+		pointerWasSet := false
+		for ef.Kind() == reflect.Ptr {
+			if ef.IsNil() {
+				if ef.Type().Elem().Kind() != reflect.Struct {
+					// This is a nil pointer to something that isn't a struct, like
+					// *string. Move along.
+					break
+				}
+
+				isNilStructPtr = true
+				// Use an empty struct of the type so we can traverse.
+				ef = reflect.New(ef.Type().Elem()).Elem()
+			} else {
+				pointerWasSet = true
+				ef = ef.Elem()
+			}
+		}
+
+		// Special case handle structs. This has to come after the value resolution in
+		// case the struct has a custom decoder.
+		if ef.Kind() == reflect.Struct {
+			for ef.CanAddr() {
+				ef = ef.Addr()
+			}
+
+			// Lookup the value, ignoring an error if the key isn't defined. This is
+			// required for nested structs that don't declare their own `env` keys,
+			// but have internal fields with an `env` defined.
+			val, found, usedDefault, err := lookup(key, required, opts.Default, l)
+			if err != nil && !errors.Is(err, ErrMissingKey) {
+				return fmt.Errorf("%s: %w", tf.Name, err)
+			}
+
+			if found || usedDefault || decodeUnset {
+				if ok, err := processAsDecoder(val, ef); ok {
+					if err != nil {
+						return err
+					}
+
+					setNilStruct(ef)
+					continue
+				}
+			}
+
+			plu := l
+			if opts.Prefix != "" {
+				plu = PrefixLookuper(opts.Prefix, l)
+			}
+
+			if err := processWith(ctx, &Config{
+				Target:           ef.Interface(),
+				Lookuper:         plu,
+				DefaultDelimiter: delimiter,
+				DefaultSeparator: separator,
+				DefaultNoInit:    noInit,
+				DefaultOverwrite: overwrite,
+				DefaultRequired:  required,
+				Mutators:         mutators,
+			}); err != nil {
+				return fmt.Errorf("%s: %w", tf.Name, err)
+			}
+
+			setNilStruct(ef)
+			continue
+		}
+
+		// It's invalid to have a prefix on a non-struct field.
+		if opts.Prefix != "" {
+			return ErrPrefixNotStruct
+		}
+
+		// Stop processing if there's no env tag (this comes after nested parsing),
+		// in case there's an env tag in an embedded struct.
+		if tag == "" {
+			continue
+		}
+
+		// The field already has a non-zero value and overwrite is false, do not
+		// overwrite.
+		if (pointerWasSet || !ef.IsZero()) && !overwrite {
+			continue
+		}
+
+		val, found, usedDefault, err := lookup(key, required, opts.Default, l)
+		if err != nil {
+			return fmt.Errorf("%s: %w", tf.Name, err)
+		}
+
+		// If the field already has a non-zero value and there was no value directly
+		// specified, do not overwrite the existing field. We only want to overwrite
+		// when the envvar was provided directly.
+		if (pointerWasSet || !ef.IsZero()) && !found {
+			continue
+		}
+
+		// Apply any mutators. Mutators are applied after the lookup, but before any
+		// type conversions. They always resolve to a string (or error), so we don't
+		// call mutators when the environment variable was not set.
+		if found || usedDefault {
+			originalKey := key
+			resolvedKey := originalKey
+			if keyer, ok := l.(keyedLookuper); ok {
+				resolvedKey = keyer.Key(resolvedKey)
+			}
+			originalValue := val
+			stop := false
+
+			for _, mu := range mutators {
+				val, stop, err = mu.EnvMutate(ctx, originalKey, resolvedKey, originalValue, val)
+				if err != nil {
+					return fmt.Errorf("%s: %w", tf.Name, err)
+				}
+				if stop {
+					break
+				}
+			}
+		}
+
+		// Set value.
+		if err := processField(val, ef, delimiter, separator, noInit); err != nil {
+			return fmt.Errorf("%s(%q): %w", tf.Name, val, err)
+		}
+	}
+
+	return nil
+}
+
+// SplitString splits the given string on the provided rune, unless the rune is
+// escaped by the escape character.
+func splitString(s, on, esc string) []string {
+	a := strings.Split(s, on)
+
+	for i := len(a) - 2; i >= 0; i-- {
+		if strings.HasSuffix(a[i], esc) {
+			a[i] = a[i][:len(a[i])-len(esc)] + on + a[i+1]
+			a = append(a[:i+1], a[i+2:]...)
+		}
+	}
+	return a
+}
+
+// keyAndOpts parses the given tag value (e.g. env:"foo,required") and
+// returns the key name and options as a list.
+func keyAndOpts(tag string) (string, *options, error) {
+	parts := splitString(tag, ",", "\\")
+	key, tagOpts := strings.TrimSpace(parts[0]), parts[1:]
+
+	if key != "" && !validateEnvName(key) {
+		return "", nil, fmt.Errorf("%q: %w ", key, ErrInvalidEnvvarName)
+	}
+
+	var opts options
+
+LOOP:
+	for i, o := range tagOpts {
+		o = strings.TrimLeftFunc(o, unicode.IsSpace)
+		search := strings.ToLower(o)
+
+		switch {
+		case search == optDecodeUnset:
+			opts.DecodeUnset = true
+		case search == optOverwrite:
+			opts.Overwrite = true
+		case search == optRequired:
+			opts.Required = true
+		case search == optNoInit:
+			opts.NoInit = true
+		case strings.HasPrefix(search, optPrefix):
+			opts.Prefix = strings.TrimPrefix(o, optPrefix)
+		case strings.HasPrefix(search, optDelimiter):
+			opts.Delimiter = strings.TrimPrefix(o, optDelimiter)
+		case strings.HasPrefix(search, optSeparator):
+			opts.Separator = strings.TrimPrefix(o, optSeparator)
+		case strings.HasPrefix(search, optDefault):
+			// If a default value was given, assume everything after is the provided
+			// value, including comma-seprated items.
+			o = strings.TrimLeft(strings.Join(tagOpts[i:], ","), " ")
+			opts.Default = strings.TrimPrefix(o, optDefault)
+			break LOOP
+		default:
+			return "", nil, fmt.Errorf("%q: %w", o, ErrUnknownOption)
+		}
+	}
+
+	return key, &opts, nil
+}
+
+// lookup looks up the given key using the provided Lookuper and options. The
+// first boolean parameter indicates whether the value was found in the
+// lookuper. The second boolean parameter indicates whether the default value
+// was used.
+func lookup(key string, required bool, defaultValue string, l Lookuper) (string, bool, bool, error) {
+	if key == "" {
+		// The struct has something like `env:",required"`, which is likely a
+		// mistake. We could try to infer the envvar from the field name, but that
+		// feels too magical.
+		return "", false, false, ErrMissingKey
+	}
+
+	if required && defaultValue != "" {
+		// Having a default value on a required value doesn't make sense.
+		return "", false, false, ErrRequiredAndDefault
+	}
+
+	// Lookup value.
+	val, found := l.Lookup(key)
+	if !found {
+		if required {
+			if keyer, ok := l.(keyedLookuper); ok {
+				key = keyer.Key(key)
+			}
+
+			return "", false, false, fmt.Errorf("%w: %s", ErrMissingRequired, key)
+		}
+
+		if defaultValue != "" {
+			// Expand the default value. This allows for a default value that maps to
+			// a different environment variable.
+			val = os.Expand(defaultValue, func(i string) string {
+				lookuper := l
+				if v, ok := lookuper.(unwrappableLookuper); ok {
+					lookuper = v.Unwrap()
+				}
+
+				s, ok := lookuper.Lookup(i)
+				if ok {
+					return s
+				}
+				return ""
+			})
+
+			return val, false, true, nil
+		}
+	}
+
+	return val, found, false, nil
+}
+
+// processAsDecoder processes the given value as a decoder or custom
+// unmarshaller.
+func processAsDecoder(v string, ef reflect.Value) (bool, error) {
+	// Keep a running error. It's possible that a property might implement
+	// multiple decoders, and we don't know *which* decoder will succeed. If we
+	// get through all of them, we'll return the most recent error.
+	var imp bool
+	var err error
+
+	// Resolve any pointers.
+	for ef.CanAddr() {
+		ef = ef.Addr()
+	}
+
+	if ef.CanInterface() {
+		iface := ef.Interface()
+
+		// If a developer chooses to implement the Decoder interface on a type,
+		// never attempt to use other decoders in case of failure. EnvDecode's
+		// decoding logic is "the right one", and the error returned (if any)
+		// is the most specific we can get.
+		if dec, ok := iface.(Decoder); ok {
+			imp = true
+			err = dec.EnvDecode(v)
+			return imp, err
+		}
+
+		if tu, ok := iface.(encoding.TextUnmarshaler); ok {
+			imp = true
+			if err = tu.UnmarshalText([]byte(v)); err == nil {
+				return imp, nil
+			}
+		}
+
+		if tu, ok := iface.(json.Unmarshaler); ok {
+			imp = true
+			if err = tu.UnmarshalJSON([]byte(v)); err == nil {
+				return imp, nil
+			}
+		}
+
+		if tu, ok := iface.(encoding.BinaryUnmarshaler); ok {
+			imp = true
+			if err = tu.UnmarshalBinary([]byte(v)); err == nil {
+				return imp, nil
+			}
+		}
+
+		if tu, ok := iface.(gob.GobDecoder); ok {
+			imp = true
+			if err = tu.GobDecode([]byte(v)); err == nil {
+				return imp, nil
+			}
+		}
+	}
+
+	return imp, err
+}
+
+func processField(v string, ef reflect.Value, delimiter, separator string, noInit bool) error {
+	// If the input value is empty and initialization is skipped, do nothing.
+	if v == "" && noInit {
+		return nil
+	}
+
+	// Handle pointers and uninitialized pointers.
+	for ef.Type().Kind() == reflect.Ptr {
+		if ef.IsNil() {
+			ef.Set(reflect.New(ef.Type().Elem()))
+		}
+		ef = ef.Elem()
+	}
+
+	tf := ef.Type()
+	tk := tf.Kind()
+
+	// Handle existing decoders.
+	if ok, err := processAsDecoder(v, ef); ok {
+		return err
+	}
+
+	// We don't check if the value is empty earlier, because the user might want
+	// to define a custom decoder and treat the empty variable as a special case.
+	// However, if we got this far, none of the remaining parsers will succeed, so
+	// bail out now.
+	if v == "" {
+		return nil
+	}
+
+	switch tk {
+	case reflect.Bool:
+		b, err := strconv.ParseBool(v)
+		if err != nil {
+			return err
+		}
+		ef.SetBool(b)
+	case reflect.Float32, reflect.Float64:
+		f, err := strconv.ParseFloat(v, tf.Bits())
+		if err != nil {
+			return err
+		}
+		ef.SetFloat(f)
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
+		i, err := strconv.ParseInt(v, 0, tf.Bits())
+		if err != nil {
+			return err
+		}
+		ef.SetInt(i)
+	case reflect.Int64:
+		// Special case time.Duration values.
+		if tf.PkgPath() == "time" && tf.Name() == "Duration" {
+			d, err := time.ParseDuration(v)
+			if err != nil {
+				return err
+			}
+			ef.SetInt(int64(d))
+		} else {
+			i, err := strconv.ParseInt(v, 0, tf.Bits())
+			if err != nil {
+				return err
+			}
+			ef.SetInt(i)
+		}
+	case reflect.String:
+		ef.SetString(v)
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		i, err := strconv.ParseUint(v, 0, tf.Bits())
+		if err != nil {
+			return err
+		}
+		ef.SetUint(i)
+
+	case reflect.Interface:
+		return fmt.Errorf("cannot decode into interfaces")
+
+	// Maps
+	case reflect.Map:
+		vals := strings.Split(v, delimiter)
+		mp := reflect.MakeMapWithSize(tf, len(vals))
+		for _, val := range vals {
+			pair := strings.SplitN(val, separator, 2)
+			if len(pair) < 2 {
+				return fmt.Errorf("%s: %w", val, ErrInvalidMapItem)
+			}
+			mKey, mVal := strings.TrimSpace(pair[0]), strings.TrimSpace(pair[1])
+
+			k := reflect.New(tf.Key()).Elem()
+			if err := processField(mKey, k, delimiter, separator, noInit); err != nil {
+				return fmt.Errorf("%s: %w", mKey, err)
+			}
+
+			v := reflect.New(tf.Elem()).Elem()
+			if err := processField(mVal, v, delimiter, separator, noInit); err != nil {
+				return fmt.Errorf("%s: %w", mVal, err)
+			}
+
+			mp.SetMapIndex(k, v)
+		}
+		ef.Set(mp)
+
+	// Slices
+	case reflect.Slice:
+		// Special case: []byte
+		if tf.Elem().Kind() == reflect.Uint8 {
+			ef.Set(reflect.ValueOf([]byte(v)))
+		} else {
+			vals := strings.Split(v, delimiter)
+			s := reflect.MakeSlice(tf, len(vals), len(vals))
+			for i, val := range vals {
+				val = strings.TrimSpace(val)
+				if err := processField(val, s.Index(i), delimiter, separator, noInit); err != nil {
+					return fmt.Errorf("%s: %w", val, err)
+				}
+			}
+			ef.Set(s)
+		}
+	}
+
+	return nil
+}
+
+// validateEnvName validates the given string conforms to being a valid
+// environment variable.
+//
+// Per IEEE Std 1003.1-2001 environment variables consist solely of uppercase
+// letters, digits, and _, and do not begin with a digit.
+func validateEnvName(s string) bool {
+	if s == "" {
+		return false
+	}
+
+	for i, r := range s {
+		if (i == 0 && !isLetter(r) && r != '_') || (!isLetter(r) && !isNumber(r) && r != '_') {
+			return false
+		}
+	}
+
+	return true
+}
+
+// isLetter returns true if the given rune is a letter between a-z,A-Z. This is
+// different than unicode.IsLetter which includes all L character cases.
+func isLetter(r rune) bool {
+	return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
+}
+
+// isNumber returns true if the given run is a number between 0-9. This is
+// different than unicode.IsNumber in that it only allows 0-9.
+func isNumber(r rune) bool {
+	return r >= '0' && r <= '9'
+}
diff --git a/vendor/github.com/sethvargo/go-envconfig/mutator.go b/vendor/github.com/sethvargo/go-envconfig/mutator.go
new file mode 100644
index 0000000..ccfadba
--- /dev/null
+++ b/vendor/github.com/sethvargo/go-envconfig/mutator.go
@@ -0,0 +1,76 @@
+// Copyright The envconfig Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package envconfig
+
+import "context"
+
+// Mutator is the interface for a mutator function. Mutators act like middleware
+// and alter values for subsequent processing. This is useful if you want to
+// mutate the environment variable value before it's converted to the proper
+// type.
+//
+// Mutators are only called on defined values (or when decodeunset is true).
+type Mutator interface {
+	// EnvMutate is called to alter the environment variable value.
+	//
+	//   - `originalKey` is the unmodified environment variable name as it was defined
+	//     on the struct.
+	//
+	//   - `resolvedKey` is the fully-resolved environment variable name, which may
+	//     include prefixes or modifications from processing. When there are
+	//     no modifications, this will be equivalent to `originalKey`.
+	//
+	//   - `originalValue` is the unmodified environment variable's value before any
+	//     mutations were run.
+	//
+	//   - `currentValue` is the currently-resolved value, which may have been
+	//     modified by previous mutators and may be modified in the future by
+	//     subsequent mutators in the stack.
+	//
+	// The function returns (in order):
+	//
+	//   - The new value to use in both future mutations and final processing.
+	//
+	//   - A boolean which indicates whether future mutations in the stack should be
+	//     applied.
+	//
+	//   - Any errors that occurred.
+	//
+	EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error)
+}
+
+var _ Mutator = (MutatorFunc)(nil)
+
+// MutatorFunc implements the [Mutator] and provides a quick way to create an
+// anonymous function.
+type MutatorFunc func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error)
+
+// EnvMutate implements [Mutator].
+func (m MutatorFunc) EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) {
+	return m(ctx, originalKey, resolvedKey, originalValue, currentValue)
+}
+
+// LegacyMutatorFunc is a helper that eases the transition from the previous
+// MutatorFunc signature. It wraps the previous-style mutator function and
+// returns a new one. Since the former mutator function had less data, this is
+// inherently lossy.
+//
+// Deprecated: Use [MutatorFunc] instead.
+func LegacyMutatorFunc(fn func(ctx context.Context, key, value string) (string, error)) MutatorFunc {
+	return func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) {
+		v, err := fn(ctx, originalKey, currentValue)
+		return v, true, err
+	}
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index bded61b..e118c85 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -139,6 +139,10 @@ github.com/remyoudompheng/bigfft
 github.com/rs/zerolog
 github.com/rs/zerolog/internal/cbor
 github.com/rs/zerolog/internal/json
+github.com/rs/zerolog/log
+# github.com/sethvargo/go-envconfig v1.1.1
+## explicit; go 1.20
+github.com/sethvargo/go-envconfig
 # github.com/stretchr/testify v1.9.0
 ## explicit; go 1.17
 github.com/stretchr/testify/assert