Repository: moov-io/customers
Pull request #253 (2020-10-19 15:25:49)

Use base database
As part of #120 with this PR we are replacing internal/database with base/database package. Major change is that now we have `/migations` where all migrations are stored.

Some issues/concerns:

- base/config can't be used out of the box right now as a replacement for Customers' configuration as it does not support environment variables. It reads config from the file. After we update base/config to work with env variables then we can replace Config in customers. Temporary Config is created.
- we should backup production DB as I have no idea how this new migrations may affect production DB (as all migrations will be run on deploy) 🤷‍♂️ -> to avoid this we can manually create (schema_migrations table and insert value of last migration (15))...
- I don't know how OSS users will be affected by this migration (they may be on different versions, etc.). I don't see the easy way to migrate. (here is an raw idea) -> One way would be to ask them to upgrade to 0.4 version if they still are not, in 0.5 we can create schema_migrations table and insert number so next time migrations from /migrations will be skipped, in 0.6 we can migrate, ...


Tasks:

- [x] replace internal/database with base/database

- [x] create separate migration files in /migrations

- [x] update makefile to generate file with embedded migrations (they are embedded into go file using [pkger](https://github.com/moovfinancial/engineering-guide/pull/26/files))

- [x] fix all loggers to follow new base/log interface

- [x] squash migrations

- [ ] create instructions on how to update to v0.5 (dump; drop; create;load)

- [ ] update docs with instructions on how to embed migrations (install pkger; make embed-migrations) 





adamdecaf: re: deployment in prod - I'm going to take a backup and move the tables to backfill with the new schemas. Will take a few minutes to do, so I'm not worried. 

For OSS folks we will announce that v0.5 is a breaking change with this and they'll need to follow steps similar to what I end up doing. 
alovak: @adamdecaf 

> re: deployment in prod - I'm going to take a backup and move the tables to backfill with the new schemas. Will take a few minutes to do, so I'm not worried.

then we have to manually create migration table (schema_migrations) and insert number of the latest migration there... or our application during the next run will run all migrations again.

is it ok?
adamdecaf: We don't need to manually create any tables. The migrator library creates them for us. 
alovak: Oh. Rigth. So I'll add it to /migrations
codecov-io: # [Codecov](https://codecov.io/gh/moov-io/customers/pull/253?src=pr&el=h1) Report
> Merging [#253](https://codecov.io/gh/moov-io/customers/pull/253?src=pr&el=desc) into [master](https://codecov.io/gh/moov-io/customers/commit/0058486386c963d5c894d4125ec6e6171e91a2b6?el=desc) will **decrease** coverage by `0.33%`.
> The diff coverage is `53.53%`.


```diff
@@            Coverage Diff             @@
##           master     #253      +/-   ##
==========================================
- Coverage   61.81%   61.48%   -0.34%     
==========================================
  Files          52       50       -2     
  Lines        2988     2905      -83     
==========================================
- Hits         1847     1786      -61     
+ Misses        819      806      -13     
+ Partials      322      313       -9     
```


Patch:
diff --git a/cmd/server/main.go b/cmd/server/main.go
index b490ae6..9f56207 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -20,13 +20,14 @@ import (
 	moovhttp "github.com/moov-io/base/http"
 	"github.com/moov-io/base/http/bind"
 
+	"github.com/moov-io/base/database"
 	mainPkg "github.com/moov-io/customers"
-	"github.com/moov-io/customers/internal/database"
 
 	"github.com/moov-io/base/log"
 
 	"github.com/moov-io/customers/internal/util"
 	"github.com/moov-io/customers/pkg/accounts"
+	"github.com/moov-io/customers/pkg/config"
 	"github.com/moov-io/customers/pkg/configuration"
 	"github.com/moov-io/customers/pkg/customers"
 	"github.com/moov-io/customers/pkg/documents"
@@ -63,8 +64,8 @@ func main() {
 		logger = log.NewDefaultLogger()
 	}
 
-	logger = logger.WithKeyValue("app", "customers")
-	logger.WithKeyValue("phase", "startup").Log(fmt.Sprintf("Starting moov-io/customers server version %s", mainPkg.Version))
+	logger = logger.Set("app", "customers")
+	logger.Set("phase", "startup").Logf("Starting moov-io/customers server version %s", mainPkg.Version)
 
 	// Channel for errors
 	errs := make(chan error)
@@ -77,20 +78,22 @@ func main() {
 
 	// Setup SQLite database
 	if sqliteVersion, _, _ := sqlite3.Version(); sqliteVersion != "" {
-		logger.Log(fmt.Sprintf("sqlite version %s", sqliteVersion))
+		logger.Logf("sqlite version %s", sqliteVersion)
 	}
 
-	// Setup database connection
-	db, err := database.New(logger, os.Getenv("DATABASE_TYPE"))
+	conf := config.New()
+	if err := conf.Load(); err != nil {
+		logger.LogErrorf("failed to load application config: %v", err)
+		os.Exit(1)
+	}
+
+	ctx := context.TODO()
+	db, err := database.NewAndMigrate(ctx, logger, *conf.Database)
 	if err != nil {
-		logger.LogError("failed to connect to database", err)
+		logger.LogErrorf("failed to connect to database: %v", err)
 		os.Exit(1)
 	}
-	defer func() {
-		if err := db.Close(); err != nil {
-			logger.LogError("failed to close database connection", err)
-		}
-	}()
+	defer db.Close()
 
 	accountsRepo := accounts.NewRepo(logger, db)
 	customerRepo := customers.NewCustomerRepo(logger, db)
@@ -103,9 +106,9 @@ func main() {
 	adminServer := admin.NewServer(*adminAddr)
 	adminServer.AddVersionHandler(mainPkg.Version) // Setup 'GET /version'
 	go func() {
-		logger.Log(fmt.Sprintf("admin listening on %s", adminServer.BindAddr()))
+		logger.Logf("admin listening on %s", adminServer.BindAddr())
 		if err := adminServer.Listen(); err != nil {
-			logger.LogErrorF("problem starting admin http: %v", err)
+			logger.LogErrorf("problem starting admin http: %v", err)
 			errs <- err
 		}
 	}()
@@ -136,8 +139,7 @@ func main() {
 	documents.AddDisclaimerAdminRoutes(logger, adminServer, disclaimerRepo, documentRepo)
 
 	// Setup Customer SSN storage wrapper
-	ctx := context.Background()
-	keeper, err := secrets.OpenSecretKeeper(ctx, "customer-ssn", os.Getenv("SSN_SECRET_PROVIDER"), os.Getenv("SSN_SECRET_KEY"))
+	keeper, err := secrets.OpenSecretKeeper(context.Background(), "customer-ssn", os.Getenv("SSN_SECRET_PROVIDER"), os.Getenv("SSN_SECRET_KEY"))
 	if err != nil {
 		panic(err)
 	}
@@ -218,21 +220,21 @@ func main() {
 	}
 	shutdownServer := func() {
 		if err := serve.Shutdown(context.TODO()); err != nil {
-			logger.WithKeyValue("phase", "shutdown").LogError("failed to shutdown server", err)
+			logger.Set("phase", "shutdown").LogErrorf("failed to shutdown server: %v", err)
 		}
 	}
 
 	// Start business logic HTTP server
 	go func() {
 		if certFile, keyFile := os.Getenv("HTTPS_CERT_FILE"), os.Getenv("HTTPS_KEY_FILE"); certFile != "" && keyFile != "" {
-			logger.WithKeyValue("phase", "startup").Log(fmt.Sprintf("binding to %s for secure HTTP server", *httpAddr))
+			logger.Set("phase", "startup").Logf("binding to %s for secure HTTP server", *httpAddr)
 			if err := serve.ListenAndServeTLS(certFile, keyFile); err != nil {
-				logger.LogError("failed to start TLS server", err)
+				logger.LogErrorf("failed to start TLS server: %v", err)
 			}
 		} else {
-			logger.WithKeyValue("phase", "startup").Log(fmt.Sprintf("binding to %s for HTTP server", *httpAddr))
+			logger.Set("phase", "startup").Logf("binding to %s for HTTP server", *httpAddr)
 			if err := serve.ListenAndServe(); err != nil {
-				logger.LogError("failed to start server", err)
+				logger.LogErrorf("failed to start server: %v", err)
 			}
 		}
 	}()
@@ -240,7 +242,7 @@ func main() {
 	// Block/Wait for an error
 	if err := <-errs; err != nil {
 		shutdownServer()
-		logger.LogError("service error", err)
+		logger.LogErrorf("service error: %v", err)
 	}
 }
 
diff --git a/cmd/server/pkged.go b/cmd/server/pkged.go
new file mode 100644
index 0000000..8bfba2f
--- /dev/null
+++ b/cmd/server/pkged.go
@@ -0,0 +1,12 @@
+// Code generated by pkger; DO NOT EDIT.
+
+// +build !skippkger
+
+package main
+
+import (
+	"github.com/markbates/pkger"
+	"github.com/markbates/pkger/pkging/mem"
+)
+
+var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec9dff6fab38b6c0ff952b7e7d99d6361870a4fd21a40d497bcb4e680b84d5a8e25b8106425e2069d3d1fddf9f800448db24ce5333abddc123cd25e6d860c09f737c7cecfec984b3e72465ba7f327e98054bfbc249e2cb384956bf85c9a5b34cb324f616c5f9ab70c17499cbc7d45ba4975694acace9a5ebad0ae186648719c5f36491fd6e6501d33d5c6b8751acd863ba4c33eb2a71982ec37498076be17b5979ac26494677f53b2b7302a6fb2fe682f9a3c3dc6756e431dd6cb1f4363f54cf4a9319d3656649f6239ca59915459efbc35e663fac951546961d793fc2d90f7b1946ee0fc772028fe930723208232fcdeb7513e7c24f980e936656b64ccbe395b748c36496fff863fb0452a63b5b465187b9f2e6d5f1839766556575d687127789bbcceffb4f86ee31de59e16cdb4aeaf7242777894b217ae9271771e21625b4b29d4c97811790637efdfad5619ecbc61cfe82ba9771e82fac2c4c66856cfed9e5ffba5e66855191352bbf85865c8749c3778fe96281ef3071e27a4c17414ee0440e72b0c879cac2a2100208fc06c16f083e40b10b5017c30b01212cb24810fe07a02e004c8709d327377f3ae5834ad7e577edad982ee405414008168fdecb7f8b82d06194289c4d992e143bcc6896305d8ee510e45900d90ef318ba4c1703d861e4fc08810ea3ba795da0c3fc6eb94f8e9f3c01a6fb2fd029fefba3c3f4f29b4de79e935ff63eff07f28045848702ec304a9ae7881c00022204feea307714f255237f7598fe89f252b8c882afca20208a4014aa7b4202e64580f332f7f50b91a2c499a6457ba5685abe290e10bec30c22cb2f4fc8deacf8f7673ab7165e7138de1ce60fe68fe2030abc450b9816305f03a6c3cc8b3bfc93f97dea53bfef266d7e7518d7caac6dd3f3af6f96d515d6858aabd142ecf2e2eafee93e4b16de619cd5625b9af19013b738e3726c7cc131481e20df455c17a00b0444000441e03e71ecd98ad223206359288a35c81a1c631112f037720ce668ae98c1430c59c0b3642fc72014380e6de5ab46eee5d87ef9fd1c8302c242a30ce6f362b0e258f92ab62083fc5f4ab24b6b1e7ea459fd3196270fb1aa6651f9616f50b4f9287659d4244c29dd22841e218dbe5ec184e9e5e9f1069a57d7bd9e54fc1ae5ffbb2e0efb72ef60f22bf93da95f1f1635e65718f77a52d01b8e7bd6585af564aee7bc1ebecad7a9a8efb62783bc9e596f38ea4dc612e8c9d39efd2a2d7b4365ea20259dc464656f5ad3a636b5a94d6d6a539bdad4a6ffd034de1ef83fffbd37d2a636b5a94d7fb334ae46f652afc2f1753ddc1f5799529d79dd0477f95baaebdb3811a43af3baf62c8cab4ca9cebcae327be32a53aa33af6bc7c4b8ca94eacc6ba96e4f9529d599d7b55f43ad8ea43a73b0e7e1b4a94d6d6a5391aeb607d7bd34c747018dc0ffb7de539bdad4a636fd372449ba561fd4d254938ec81676682159ce6a15f28da927a9aee3ba9ed61a5799fddde2bdea7c6bbbb6a94d6dfacf4dfff807f37d214500c02767e15999f754095c2ce717e9ff4687838c0e15dc861d71101f8b3ada8d9e048813041e7f6bd411cb0980b0e49ba327d92aea08039ef00851074f6eda481d3c59cbef0d3afa5c0612c88b5c157454bc886dcc91d8861cb521475f851c1dead27510d284bd8926861a8d866a3089dfa291ac40875557b6aead9d752fe9873d7fc26aefae4c32d3b879b6f4b1efc683f504058113fae0ae3f4d47bdc41fc9e6dc9929c04038b0f5c73d32706eca6a6a6a6439d161341a9a81132bc9c41825ca556ffeb3dfbb1df5a574621cab07cf27287bb6e34166de4b6862dcbc58f2607d7b357ebdbd7ff5f37b76582d36e3886b5ee36ebd2d7f3377666a62203570e547df9407c034d4b9ad3f96e7870a98182a74d6cdba4755dda60e034b7f6ddedbdbdd4b75ffc033a49db6ddbd3cce7f16d7256b130d9696310f5c395ad9e1c77b9796363bf6ed9996dafdd7bcae1727d60257d6a6061a80919cdfaf062c1d46bbcf0aae4c398a2d5d9b7e213335f5b7e8401daf4e1c6513e3068fe42cf2ee7bc987f73def8753a1ef7fb3a2429f3ecea77990cc3c5a7d75b4fc566d41819ca8b638880161bf5f6d71e06c6a8be33842a040afb6ca36d2abad4afe04b5c5f24484b5da2a5e44abb65ab575446d1dedd994da0b92574b264bd3b8f36f0bfad6e70c144d4703537a9c2aa3b1b6a37d96ae0e43d318f9da74703f0681f418faeb4af30ccdc096a3e9e8fae6f707f036183f729b7c153bf2a36fe91898ba1b3961a92926882c1d565d4ff468e9f6a517d750808d60e444a4ba8eabe3b96368d1a81f34cfcfcdfe6bae09b249acad6fc7c9fc9fafc9f71298fdfc9c2dd75d78694a0d619a2ab61c6605742287314f20f8de450b2587e1f9860f024b0470c2f0a16c233d872bf913388c514ed88ac3c58b6839dc72f80887693a37358a035356d7a6a1bc9bc65d39a8d0d5a9136bef0ecc51f7c910fe60946e704a3310d9a0f81140e9e1faede6211a6f8cfec1d41cde440e7bf7b6639c3fd4867d754fefa37dc6fd8b65a87867e0f2509d7b77f381c4ce8064b43df76ab30a98e86fcf1344a6bb32d3aa6e9bd596eef0067f3968d9a89efc199b7da9ff00c89dfa38f8e75123bf547d2b7ba600271eccedd978e739566d5e4bd08ea337577ff46f3fa8ca9f7da97a7f77f7d3dbdb87f4f65b5511f7f1537b8abdcccabf2c4a4574bc826a3820c213d5104f044144675043e85c6a088b3ccb11884fd0436523e9f550257f821ee231c7927aed5cf1265a3dd4eaa1237ae878dfa6d3420682912b0f7282065f0e08be764765a6a10606cab5d0d7ae265bd7c0442368a2bfc15db23fbedd855b97d37e026fca2f4c237a76e3413a1a6a4bcb1840f3be97d4e7a6e9482eeebf9071f5c7f3b865f0f661976bc79f9673d7caa8c703474a570ce64e1d0a881ccb9e612281fbee6d182a0473221178c089f4042e9a08e8095cc99f406041041cdb2030d78e045a021f27f0916e4d895f76e35587043aa581fa7e028663372f1b6bcf0eab06ee507b1fc9d1d295b599698c6ac4ea30caf1da30dee1ddc3a836dee5c1d2449f8de4b3a094ffa4b79267cb794a3d6be104d448a5ac658b5684f8d3d00a11cb627c0e2f0b7746f3168818239e1aae9b4652c3b596a7872b843c44a0dee2a678132d5c5bb81e812b6507a7b571493cd19567076905442b6f0bd514aeb674e50878da571e99cdb42a529707a76687cad48e483905fa11d04329726225b2676a6022edd9d60760827cdf94092cda3194d6a6aecc1d14adecb09728f7af7e6d3fdfa4365216a63ef68b7d25642db0c3c353a46781baf0e96da5e98c12e507cb56b6317ba27f0272acc873e40c00c7670438122111e8f728db34921ee095fc09006709144063be926dfd132dc08f03fc60b7a6b68ddfedd02f70b63b0748e71877586539babeb97b005bd42aef7644b2895122b3816a5c39bb37f7630e48e0f6a5c2b1ecca51669e63ae51dc3e23377196b137cb68cddffd052b6092139d09101320a2733874f9b301130082204befcfddb4919e9795fc09bcc41c8fd9dae02d5e44cbcb96974778b9bf4b1f82a5ba9ab05a66ea1838eb0294531b29d0d6b5a53b38cf0ce2be19401b6190df87c17e926bd4a1bdda3209ccbd33726a64cb1a3075f579628c9b2188df3fbb46aa071ea64e64852744851f2a5a01189f186007050e10f61c2e07e16cee5c827980104b0fe0b28df400aee44f00304f0484480d60dc06d8b5003e0ee0439dfa2082e7c5b68e7a94e373e3bbddc9fb0aa9be33bc99dbfa60eddd4be5345689d8c81b8e7d57d638b7bf89aa96c98b5bf86fd5032e0a656dea83afb0b939ffd7611582cf8ff1c9721c6f9e593387d6bf4b5b4beddf1528610bba2c7f21624c589683df6dedf28427df0b5bc0565bff12c8222862011fa22d60f96a7bf16d230fd1768ffc41daee9611798e08a4e1df155adab6b43d465bda0e7e08bc513c92f1caed4b912747efeef0ce37e5e87d82de82dc9e7422124c9012394335b06325dadac796a1bcd8f2607e6462ed88c3611b19a7bc98867400bcbbf10d87eecf6037f10d1a013624f5f5c36604da27c5927e9c283c475034acd64c598e932ca9fd14fbcb55ab27092db8b756320108f3fc19ac64f17c6e0a01f3903d615eae6c23bd955cc99f60258b58243ca8574f9296db2db78f737b6f8f3e44eac1cb041168cfd48d795cfda6378de9c221a0830e9e8fec58597b5b62eb4a195b3c682c3b697a37ea72c9c450128a32cd459d8183b2c28d3c41cacad531301086b67e9313bda14dae61358b1813e8cad1d2847beea7aa3b58d9b25a2c97f9b460f4a537cfcd7d331e407b38f66fd64e5abac237e121bb31defb176736c2438e2cb63c2dc67afbfc86caf376898fc1aa2b03d5efb719e9f7c133958ee4207050b0e7b9be7ef50c371181dfac11abe5582b2b0add329b52291e2a5a0d684edd55004144083cc77ce7d97615e04444b02000fa3fc954b691fe4f3255f2f47a110196c31caac733edae02ad5ea4d08b873af501d5582f5f99db0897ea7137ef04157950ad52ade12f56651a6a64b2d366f9e64acfc88ed5d5ae0a6b2c15da377bba39ef1a65de5f31bb0ad927279859fece2b79b25cf7299cb9de1b25ace92ad9629bd0527be386e2b100be7d2b189e08e07bd7d437bd501cc280407430847bc74354b6f16008f71e797a2f148f216c2ee6242db45b681f87365de7fe02dfb985296bdc488ea6dea058e33237e501b074b274e0ee6f0335b1acbe8f64b2fc88fd513ff850a65c5d585bb8855fe77bf1c87d18c9fd7fe2b0292ba9ac5a7c621836e22006e219e644f1d9361de144cc1184e9bd3d9b36d25bb595fc09562d8b08e16a6f4ff1225a40b6803c0248baee4de5fa79b691194c207937f5c2aedc3ae41b90db0dd2feca8d331a4a6b4b8781339bfa16d2f0c605b103cabdae9f993a7787d121db786a7fe552a9eb783765fcec0ea357f35e9adb33353291b6de2e65378d9b1753c760a2bb918160e0ca4a3292d595abdfa4661178a3bd588632b711e7df5e3da6a3abe32e9673046bc36add52b2f0ad59f85e9c787292d973e82f1727b9324ea9aade79eac4506ec46370867da7f0d9f63be144967058a05fe558b410d0af72ace54f500158e0448c1bfb4eb571dcad0a38ae024ee9e28714c1874dfe22f262231c5bba0b9db8b47a6fe93603fc10c1b8b33960b9f25cd66613fded3987f1a77d49722552ce1a2f5dfd2dfdb8d95f6dad7f76bad831012319435b7ea50a9b29bb8547df2fdabfc1fef7fa1becbffe0f0000ffff010000ffff00ac31be27840000`)))
diff --git a/go.mod b/go.mod
index f971752..42d6c5b 100644
--- a/go.mod
+++ b/go.mod
@@ -21,10 +21,11 @@ require (
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/hashicorp/vault/api v1.0.4
 	github.com/lopezator/migrator v0.3.0
+	github.com/markbates/pkger v0.17.1
 	github.com/mattn/go-sqlite3 v2.0.3+incompatible
 	github.com/mitchellh/mapstructure v1.3.3
 	github.com/moov-io/ach v1.4.4
-	github.com/moov-io/base v0.11.2-0.20201009172605-8a2f4aef9cc4
+	github.com/moov-io/base v0.13.1
 	github.com/moov-io/fed v0.5.1
 	github.com/moov-io/paygate v0.8.0
 	github.com/moov-io/watchman v0.15.1
diff --git a/go.sum b/go.sum
index 8483b7b..f147def 100644
--- a/go.sum
+++ b/go.sum
@@ -277,6 +277,7 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
 github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
 github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
 github.com/goftp/file-driver v0.0.0-20180502053751-5d604a0fc0c9/go.mod h1:GpOj6zuVBG3Inr9qjEnuVTgBlk2lZ1S9DcoFiXWyKss=
@@ -286,6 +287,7 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang-migrate/migrate/v4 v4.12.2 h1:QI43Tlouiwpp2dK5Y767OouX0snJNRP/NubsVaArzDU=
 github.com/golang-migrate/migrate/v4 v4.12.2/go.mod h1:HQ1DaC8uLHkg4afY8ZQ8D/P5SG+YW9X5INZBVvm+d2k=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -547,6 +549,7 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q
 github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
+github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno=
 github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@@ -609,6 +612,10 @@ github.com/moov-io/base v0.11.1 h1:5qGfUr3eKWFmjttK2WllKgBg0Pnzx50+JBnRQQi8Gl0=
 github.com/moov-io/base v0.11.1/go.mod h1:kiWd5d0G3Ji+V9VhCNyzgfMNfbpzBblSL+QY2n2eywE=
 github.com/moov-io/base v0.11.2-0.20201009172605-8a2f4aef9cc4 h1:UXNd4TzOUtIRQfUAOZsAox3UN0uj2ToEM/W8mL0jLSo=
 github.com/moov-io/base v0.11.2-0.20201009172605-8a2f4aef9cc4/go.mod h1:X3Rkm6lKCODm8zF7s43aawbyRDFyBdmEzO+NteVTj+A=
+github.com/moov-io/base v0.13.0 h1:CNDStMttpov1p1a4WuIeJsE+GUgXdlpLEUuigmrqaro=
+github.com/moov-io/base v0.13.0/go.mod h1:X3Rkm6lKCODm8zF7s43aawbyRDFyBdmEzO+NteVTj+A=
+github.com/moov-io/base v0.13.1 h1:PE/A6rm5uPf6TdupuNxI2DMobZ0+7XIWAjSj/2qv/AA=
+github.com/moov-io/base v0.13.1/go.mod h1:X3Rkm6lKCODm8zF7s43aawbyRDFyBdmEzO+NteVTj+A=
 github.com/moov-io/customers v0.4.0-rc4/go.mod h1:wNz/qIiG24rJrjnK3VOvzj0MbdfGznj6INO/uyntgNM=
 github.com/moov-io/customers v0.4.0-rc4.0.20200519011716-05c4c319c49b/go.mod h1:wmBTMaZ7OzoXhDtLqGngNrEaRlsk7VFLxY0kl3HzoHc=
 github.com/moov-io/customers v0.4.0-rc5.0.20200605165738-057f20d4d580/go.mod h1:2viqzwFr1cp5gXL9lhOIXl7KE1pDIA8Z4I5smZBRUSo=
diff --git a/internal/database/database.go b/internal/database/database.go
deleted file mode 100644
index ee98d92..0000000
--- a/internal/database/database.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2020 The Moov Authors
-// Use of this source code is governed by an Apache License
-// license that can be found in the LICENSE file.
-
-package database
-
-import (
-	"database/sql"
-	"fmt"
-	"os"
-	"strings"
-
-	"github.com/lopezator/migrator"
-	"github.com/moov-io/base/log"
-)
-
-func New(logger log.Logger, _type string) (*sql.DB, error) {
-	switch strings.ToLower(_type) {
-	case "sqlite", "":
-		return sqliteConnection(logger, getSqlitePath()).Connect()
-	case "mysql":
-		return mysqlConnection(logger, os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_ADDRESS"), os.Getenv("MYSQL_DATABASE")).Connect()
-	}
-	return nil, fmt.Errorf("unknown database type %q", _type)
-}
-
-func execsql(name, raw string) *migrator.MigrationNoTx {
-	return &migrator.MigrationNoTx{
-		Name: name,
-		Func: func(db *sql.DB) error {
-			_, err := db.Exec(raw)
-			return err
-		},
-	}
-}
-
-// UniqueViolation returns true when the provided error matches a database error
-// for duplicate entries (violating a unique table constraint).
-func UniqueViolation(err error) bool {
-	return MySQLUniqueViolation(err) || SqliteUniqueViolation(err)
-}
diff --git a/internal/database/mysql.go b/internal/database/mysql.go
deleted file mode 100644
index 6f1e234..0000000
--- a/internal/database/mysql.go
+++ /dev/null
@@ -1,302 +0,0 @@
-// Copyright 2020 The Moov Authors
-// Use of this source code is governed by an Apache License
-// license that can be found in the LICENSE file.
-
-package database
-
-import (
-	"database/sql"
-	"fmt"
-	"os"
-	"strings"
-	"testing"
-	"time"
-
-	"github.com/moov-io/base/docker"
-
-	kitprom "github.com/go-kit/kit/metrics/prometheus"
-	gomysql "github.com/go-sql-driver/mysql"
-	"github.com/lopezator/migrator"
-	"github.com/moov-io/base/log"
-	"github.com/ory/dockertest/v3"
-	stdprom "github.com/prometheus/client_golang/prometheus"
-)
-
-var (
-	mysqlConnections = kitprom.NewGaugeFrom(stdprom.GaugeOpts{
-		Name: "mysql_connections",
-		Help: "How many MySQL connections and what status they're in.",
-	}, []string{"state"})
-
-	// mySQLErrDuplicateKey is the error code for duplicate entries
-	// https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html#error_er_dup_entry
-	mySQLErrDuplicateKey uint16 = 1062
-
-	// ToDo: Consideration: For _type _status integer (1,2,3,4,5,6...) or string ("01", "1", "ABC")
-
-	mysqlMigrations = migrator.Migrations(
-		execsql(
-			"create_customers",
-			`create table if not exists customers(customer_id varchar(40), first_name varchar(40), middle_name varchar(40), last_name varchar(40), nick_name varchar(40), suffix varchar(3), birth_date datetime, status integer, email varchar(120), created_at datetime, last_modified datetime, deleted_at datetime, PRIMARY KEY (customer_id));`),
-
-		execsql(
-			"create_customers_phones",
-			`create table if not exists customers_phones (customer_id VARCHAR(40), number VARCHAR(20), valid BOOLEAN, type integer, unique (customer_id, number));`,
-		),
-		execsql(
-			"create_customers_addresses",
-			`create table if not exists customers_addresses(address_id varchar(40) primary key, customer_id varchar(40), type SMALLINT, address1 varchar(120), address2 varchar(120), city varchar(50), state varchar(2), postal_code varchar(9), country varchar(3), validated BOOLEAN, unique (customer_id, address1));`,
-		),
-		execsql(
-			"create_customer_metadata",
-			`create table if not exists customer_metadata(customer_id varchar(40), meta_key varchar(40), meta_value varchar(512), unique(meta_key, meta_value));`,
-		),
-		execsql(
-			"customer_status_updates",
-			`create table if not exists customer_status_updates(customer_id varchar(40), future_status integer, comment varchar(512), changed_at datetime);`,
-		),
-		execsql(
-			"create_customer_ofac_searches",
-			`create table if not exists customer_ofac_searches(customer_id varchar(40), entity_id varchar(40), sdn_name varchar(40), sdn_type integer, percentage_match double precision (5,2), created_at datetime);`,
-		),
-		execsql(
-			"create_customer_ssn",
-			`create table if not exists customer_ssn(customer_id varchar(40) primary key, ssn BLOB, ssn_masked varchar(9), created_at datetime);`,
-		),
-		execsql(
-			"create_documents",
-			`create table if not exists documents(document_id varchar(40) primary key, customer_id varchar(40), type varchar(120), content_type varchar(40), uploaded_at datetime);`,
-		),
-		execsql(
-			"create_disclaimers",
-			`create table if not exists disclaimers(disclaimer_id varchar(40) primary key, text text, document_id varchar(40), created_at datetime, deleted_at datetime);`,
-		),
-		execsql(
-			"create_disclaimer_acceptances",
-			`create table if not exists disclaimer_acceptances(disclaimer_id varchar(40), customer_id varchar(40), accepted_at datetime, unique(disclaimer_id, customer_id));`,
-		),
-		execsql(
-			"create_accounts",
-			`create table if not exists accounts(account_id varchar(40) primary key, customer_id varchar(40), user_id varchar(40), encrypted_account_number varchar(40), hashed_account_number varchar(40), masked_account_number varchar(15), routing_number varchar(10), status varchar(12), type varchar(12), created_at datetime, deleted_at datetime);`,
-		),
-		execsql(
-			"alter_customers_status",
-			`alter table customers modify status varchar(20);`,
-		),
-		execsql(
-			"expand_accounts_encrypted_account_number",
-			`alter table accounts modify encrypted_account_number varchar(100);`,
-		),
-		execsql(
-			"add_customer_type",
-			`alter table customers add column type varchar(25)`,
-		),
-		execsql(
-			"update_customer_type",
-			`update customers set type = 'individual' where type is null;`,
-		),
-		execsql(
-			"unique_accounts_per_customer",
-			`alter table accounts add constraint unique accounts_unique_to_customer (customer_id, hashed_account_number, routing_number);`,
-		),
-		execsql(
-			"add_holder_name_to_accounts",
-			`alter table accounts add column holder_name varchar(60) default '';`,
-		),
-		execsql(
-			"create_validations",
-			`create table if not exists validations(
-				validation_id  varchar(40) primary key,
-				account_id varchar(40),
-				status varchar(20),
-				strategy varchar(20),
-				vendor varchar(20),
-				created_at datetime,
-				updated_at datetime,
-				INDEX idx_validations_validation_account_ids (validation_id, account_id)
-			);`,
-		),
-		execsql(
-			"create_account_ofac_searches",
-			`create table if not exists account_ofac_searches(account_ofac_search_id varchar(40) primary key, account_id varchar(40), entity_id varchar(40), sdn_name varchar(40), sdn_type integer, percentage_match double precision (5,2), created_at datetime);`,
-		),
-		execsql(
-			"add_namespace__to__customers",
-			"alter table customers add column namespace varchar(40) not null;",
-		),
-		execsql(
-			"create_namespace_configuration",
-			`create table if not exists namespace_configuration(namespace varchar(40) primary key not null, legal_entity varchar(40) not null, primary_account varchar(40) not null);`,
-		),
-		execsql(
-			"add_deleted_at__to__documents",
-			"alter table documents add column deleted_at datetime;",
-		),
-		execsql(
-			"add_deleted_at__to__customers_addresses",
-			`alter table customers_addresses add column deleted_at datetime;`,
-		),
-		execsql(
-			"rename_customers_namespace_to_organization",
-			`alter table customers rename column namespace to organization;`,
-		),
-		execsql(
-			"rename_namespace_configuration__to__organization_configuration",
-			`alter table namespace_configuration rename to organization_configuration;`,
-		),
-		execsql(
-			"rename_organization_configuration__namespace_to_organization",
-			`alter table organization_configuration rename column namespace to organization;`,
-		),
-		execsql(
-			"add_blocked__to__customer_ofac_searches",
-			`alter table customer_ofac_searches add column blocked boolean;`,
-		),
-	)
-)
-
-type discardLogger struct{}
-
-func (l discardLogger) Print(v ...interface{}) {}
-
-func init() {
-	gomysql.SetLogger(discardLogger{})
-}
-
-type mysql struct {
-	dsn    string
-	logger log.Logger
-
-	connections *kitprom.Gauge
-}
-
-func (my *mysql) Connect() (*sql.DB, error) {
-	db, err := sql.Open("mysql", my.dsn)
-	if err != nil {
-		return nil, err
-	}
-
-	// Check out DB is up and working
-	if err := db.Ping(); err != nil {
-		return nil, err
-	}
-
-	migratorLogger := migrator.WithLogger(migrator.LoggerFunc(func(msg string, args ...interface{}) {
-		my.logger.WithKeyValue("package", "mysql").Log(msg)
-	}))
-
-	// Migrate our database
-	if m, err := migrator.New(migratorLogger, mysqlMigrations); err != nil {
-		return nil, err
-	} else {
-		if err := m.Migrate(db); err != nil {
-			return nil, err
-		}
-	}
-
-	// Setup metrics after the database is setup
-	go func() {
-		t := time.NewTicker(1 * time.Minute)
-		for range t.C {
-			stats := db.Stats()
-			my.connections.With("state", "idle").Set(float64(stats.Idle))
-			my.connections.With("state", "inuse").Set(float64(stats.InUse))
-			my.connections.With("state", "open").Set(float64(stats.OpenConnections))
-		}
-	}()
-
-	return db, nil
-}
-
-func mysqlConnection(logger log.Logger, user, pass string, address string, database string) *mysql {
-	timeout := "30s"
-	if v := os.Getenv("MYSQL_TIMEOUT"); v != "" {
-		timeout = v
-	}
-	params := fmt.Sprintf("timeout=%s&charset=utf8mb4&parseTime=true&sql_mode=ALLOW_INVALID_DATES", timeout)
-	dsn := fmt.Sprintf("%s:%s@%s/%s?%s", user, pass, address, database, params)
-	return &mysql{
-		dsn:         dsn,
-		logger:      logger,
-		connections: mysqlConnections,
-	}
-}
-
-// TestMySQLDB is a wrapper around sql.DB for MySQL connections designed for tests to provide
-// a clean database for each testcase.  Callers should cleanup with Close() when finished.
-type TestMySQLDB struct {
-	DB *sql.DB
-
-	container *dockertest.Resource
-}
-
-func (r *TestMySQLDB) Close() error {
-	r.container.Close()
-	return r.DB.Close()
-}
-
-// CreateTestMySQLDB returns a TestMySQLDB which can be used in tests
-// as a clean mysql database. All migrations are ran on the db before.
-//
-// Callers should call close on the returned *TestMySQLDB.
-func CreateTestMySQLDB(t *testing.T) *TestMySQLDB {
-	if testing.Short() {
-		t.Skip("-short flag enabled")
-	}
-	if !docker.Enabled() {
-		t.Skip("Docker not enabled")
-	}
-
-	pool, err := dockertest.NewPool("")
-	if err != nil {
-		t.Fatal(err)
-	}
-	resource, err := pool.RunWithOptions(&dockertest.RunOptions{
-		Repository: "mysql",
-		Tag:        "8",
-		Env: []string{
-			"MYSQL_USER=moov",
-			"MYSQL_PASSWORD=secret",
-			"MYSQL_ROOT_PASSWORD=secret",
-			"MYSQL_DATABASE=paygate",
-		},
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	err = pool.Retry(func() error {
-		db, err := sql.Open("mysql", fmt.Sprintf("moov:secret@tcp(localhost:%s)/paygate", resource.GetPort("3306/tcp")))
-		if err != nil {
-			return err
-		}
-		defer db.Close()
-		return db.Ping()
-	})
-	if err != nil {
-		resource.Close()
-		t.Fatal(err)
-	}
-
-	logger := log.NewNopLogger()
-	address := fmt.Sprintf("tcp(localhost:%s)", resource.GetPort("3306/tcp"))
-
-	db, err := mysqlConnection(logger, "moov", "secret", address, "paygate").Connect()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	t.Cleanup(func() {
-		pool.Purge(resource)
-	})
-	return &TestMySQLDB{db, resource}
-}
-
-// MySQLUniqueViolation returns true when the provided error matches the MySQL code
-// for duplicate entries (violating a unique table constraint).
-func MySQLUniqueViolation(err error) bool {
-	match := strings.Contains(err.Error(), fmt.Sprintf("Error %d: Duplicate entry", mySQLErrDuplicateKey))
-	if e, ok := err.(*gomysql.MySQLError); ok {
-		return match || e.Number == mySQLErrDuplicateKey
-	}
-	return match
-}
diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go
deleted file mode 100644
index 325c17f..0000000
--- a/internal/database/sqlite.go
+++ /dev/null
@@ -1,274 +0,0 @@
-// Copyright 2020 The Moov Authors
-// Use of this source code is governed by an Apache License
-// license that can be found in the LICENSE file.
-
-package database
-
-import (
-	"database/sql"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-	"sync"
-	"testing"
-	"time"
-
-	kitprom "github.com/go-kit/kit/metrics/prometheus"
-	"github.com/lopezator/migrator"
-	"github.com/mattn/go-sqlite3"
-	"github.com/moov-io/base/log"
-	stdprom "github.com/prometheus/client_golang/prometheus"
-)
-
-var (
-	sqliteConnections = kitprom.NewGaugeFrom(stdprom.GaugeOpts{
-		Name: "sqlite_connections",
-		Help: "How many sqlite connections and what status they're in.",
-	}, []string{"state"})
-
-	sqliteVersionLogOnce sync.Once
-
-	sqliteMigrations = migrator.Migrations(
-		execsql(
-			"create_customers",
-			`create table if not exists customers(customer_id primary key, first_name, middle_name, last_name, nick_name, suffix, birth_date datetime, status, email, created_at datetime, last_modified datetime, deleted_at datetime);`,
-		),
-		execsql(
-			"create_customers_phones",
-			`create table if not exists customers_phones(customer_id, number, valid, type, unique (customer_id, number) on conflict abort);`,
-		),
-		execsql(
-			"create_customers_addresses",
-			`create table if not exists customers_addresses(address_id primary key, customer_id, type, address1, address2, city, state, postal_code, country, validated,
-			deleted_at datetime, unique (customer_id, address1) on conflict abort);`,
-		),
-		execsql(
-			"create_customer_metadata",
-			`create table if not exists customer_metadata(customer_id, meta_key, meta_value, unique(meta_key, meta_value));`,
-		),
-		execsql(
-			"customer_status_updates",
-			`create table if not exists customer_status_updates(customer_id, future_status, comment, changed_at datetime);`,
-		),
-		execsql(
-			"create_customer_ofac_searches",
-			`create table if not exists customer_ofac_searches(customer_id, entity_id, sdn_name, sdn_type, percentage_match, created_at datetime);`,
-		),
-		execsql(
-			"create_customer_ssn",
-			`create table if not exists customer_ssn(customer_id primary key, ssn, ssn_masked, created_at datetime);`,
-		),
-		execsql(
-			"create_documents",
-			`create table if not exists documents(document_id primary key, customer_id, type, content_type, uploaded_at datetime, deleted_at datetime);`,
-		),
-		execsql(
-			"create_disclaimers",
-			`create table if not exists disclaimers(disclaimer_id primary key, text, document_id, created_at datetime, deleted_at datetime);`,
-		),
-		execsql(
-			"create_disclaimer_acceptances",
-			`create table if not exists disclaimer_acceptances(disclaimer_id, customer_id, accepted_at datetime, unique(disclaimer_id, customer_id) on conflict ignore);`,
-		),
-		execsql(
-			"create_accounts",
-			`create table if not exists accounts(account_id primary key, customer_id, user_id, encrypted_account_number, hashed_account_number, masked_account_number, routing_number, status, type, created_at datetime, deleted_at datetime);`,
-		),
-		execsql(
-			"add_customer_type",
-			`alter table customers add column type; update customers set type = 'individual' where type is null;`,
-		),
-		execsql(
-			"unique_accounts_per_customer",
-			`
-PRAGMA foreign_keys=off;
-BEGIN TRANSACTION;
-
-ALTER TABLE accounts RENAME TO accounts_old;
-
-CREATE TABLE accounts
-(
-  account_id primary key,
-  customer_id,
-  user_id,
-  encrypted_account_number,
-  hashed_account_number,
-  masked_account_number,
-  routing_number,
-  status,
-  type,
-  created_at datetime,
-  deleted_at datetime,
-  CONSTRAINT accounts_unique_to_customer UNIQUE (customer_id, hashed_account_number, routing_number)
-);
-
-INSERT INTO accounts SELECT * FROM accounts_old;
-
-COMMIT;
-
-PRAGMA foreign_keys=on;`,
-		),
-		execsql(
-			"add_holder_name_to_accounts",
-			`alter table accounts add column holder_name default '';`,
-		),
-		execsql(
-			"create_validations",
-			`create table if not exists validations(
-				validation_id primary key,
-				account_id,
-				status,
-				strategy,
-				vendor,
-				created_at datetime,
-				updated_at datetime
-			);`,
-		),
-		execsql(
-			"create validations index",
-			`
-			create unique index
-				idx_validations_validation_account_ids
-			on
-				validations (validation_id, account_id);
-			`,
-		),
-		execsql(
-			"create_account_ofac_searches",
-			`create table if not exists account_ofac_searches(account_ofac_search_id varchar(40) primary key, account_id varchar(40), entity_id varchar(40), sdn_name varchar(40), sdn_type integer, percentage_match double precision (5,2), created_at datetime);`,
-		),
-		execsql(
-			"add_namespace__to__customers",
-			"alter table customers add column namespace varchar(40) not null default 'unknown';",
-		),
-		execsql(
-			"create_organization_configuration",
-			`create table organization_configuration(organization varchar(40) primary key not null, legal_entity varchar(40) not null, primary_account varchar(40) not null);`,
-		),
-		execsql(
-			"rename_customers_namespace_to_organization",
-			`alter table customers rename column namespace to organization;`,
-		),
-		execsql(
-			"add_blocked__to__customer_ofac_searches",
-			`alter table customer_ofac_searches add column blocked boolean;`,
-		),
-	)
-)
-
-type sqlite struct {
-	path string
-
-	connections *kitprom.Gauge
-	logger      log.Logger
-
-	err error
-}
-
-func (s *sqlite) Connect() (*sql.DB, error) {
-	if s.err != nil {
-		return nil, fmt.Errorf("sqlite had error %v", s.err)
-	}
-
-	sqliteVersionLogOnce.Do(func() {
-		if v, _, _ := sqlite3.Version(); v != "" {
-			s.logger.Log(fmt.Sprintf("sqlite version %s", v))
-		}
-	})
-
-	db, err := sql.Open("sqlite3", s.path)
-	if err != nil {
-		return nil, err
-	}
-	if err := db.Ping(); err != nil {
-		return db, err
-	}
-
-	migratorLogger := migrator.WithLogger(migrator.LoggerFunc(func(msg string, args ...interface{}) {
-		s.logger.Log(fmt.Sprintf("sqlite: %s", msg))
-	}))
-
-	// Migrate our database
-	if m, err := migrator.New(migratorLogger, sqliteMigrations); err != nil {
-		return db, err
-	} else {
-		if err := m.Migrate(db); err != nil {
-			return db, err
-		}
-	}
-
-	// Spin up metrics only after everything works
-	go func() {
-		t := time.NewTicker(1 * time.Second)
-		for range t.C {
-			stats := db.Stats()
-			s.connections.With("state", "idle").Set(float64(stats.Idle))
-			s.connections.With("state", "inuse").Set(float64(stats.InUse))
-			s.connections.With("state", "open").Set(float64(stats.OpenConnections))
-		}
-	}()
-
-	return db, err
-}
-
-func sqliteConnection(logger log.Logger, path string) *sqlite {
-	return &sqlite{
-		path:        path,
-		logger:      logger,
-		connections: sqliteConnections,
-	}
-}
-
-func getSqlitePath() string {
-	path := os.Getenv("SQLITE_DB_PATH")
-	if path == "" || strings.Contains(path, "..") {
-		// set default if empty or trying to escape
-		// don't filepath.ABS to avoid full-fs reads
-		path = "customers.db"
-	}
-	return path
-}
-
-// TestSQLiteDB is a wrapper around sql.DB for SQLite connections designed for tests to provide
-// a clean database for each testcase.  Callers should cleanup with Close() when finished.
-type TestSQLiteDB struct {
-	DB *sql.DB
-
-	dir string // temp dir created for sqlite files
-}
-
-func (r *TestSQLiteDB) Close() error {
-	if err := r.DB.Close(); err != nil {
-		return err
-	}
-	return os.RemoveAll(r.dir)
-}
-
-// CreateTestSqliteDB returns a TestSQLiteDB which can be used in tests
-// as a clean sqlite database. All migrations are ran on the db before.
-//
-// Callers should call close on the returned *TestSQLiteDB.
-func CreateTestSqliteDB(t *testing.T) *TestSQLiteDB {
-	dir, err := ioutil.TempDir("", "customers-sqlite")
-	if err != nil {
-		t.Fatalf("sqlite test: %v", err)
-	}
-
-	db, err := sqliteConnection(log.NewNopLogger(), filepath.Join(dir, "customers.db")).Connect()
-	if err != nil {
-		t.Fatalf("sqlite test: %v", err)
-	}
-	return &TestSQLiteDB{db, dir}
-}
-
-// SqliteUniqueViolation returns true when the provided error matches the SQLite error
-// for duplicate entries (violating a unique table constraint).
-func SqliteUniqueViolation(err error) bool {
-	match := strings.Contains(err.Error(), "UNIQUE constraint failed")
-	if e, ok := err.(sqlite3.Error); ok {
-		return match || e.Code == sqlite3.ErrConstraint
-	}
-	return match
-}
diff --git a/makefile b/makefile
index 5bc7697..050228e 100644
--- a/makefile
+++ b/makefile
@@ -4,7 +4,7 @@ VERSION := $(shell grep -Eo '(v[0-9]+[\.][0-9]+[\.][0-9]+(-[a-zA-Z0-9]*)?)' vers
 USERID := $(shell id -u $$USER)
 GROUPID:= $(shell id -g $$USER)
 
-.PHONY: build build-server build-examples docker release check
+.PHONY: build build-server build-examples docker release check embed-migrations
 
 build: build-server
 
@@ -78,6 +78,9 @@ release-push:
 	docker push moov/customers:$(VERSION)
 	docker push moov/customers:latest
 
+embed-migrations:
+	pkger -include /migrations -o cmd/server
+
 # quay-push:
 # 	docker push quay.io/moov/customers:$(VERSION)
 # 	docker push quay.io/moov/customers:latest
diff --git a/migrations/001_create_customers.up.sql b/migrations/001_create_customers.up.sql
new file mode 100644
index 0000000..146d826
--- /dev/null
+++ b/migrations/001_create_customers.up.sql
@@ -0,0 +1,17 @@
+create table customers (
+  customer_id varchar(40), 
+  first_name varchar(40), 
+  middle_name varchar(40), 
+  last_name varchar(40), 
+  nick_name varchar(40), 
+  suffix varchar(3), 
+  birth_date datetime, 
+  status varchar(20), 
+  email varchar(120), 
+  type varchar(25),
+  organization varchar(40) not null,
+  created_at datetime, 
+  last_modified datetime, 
+  deleted_at datetime, 
+  primary key (customer_id)
+);
diff --git a/migrations/002_create_customers_phones.up.sql b/migrations/002_create_customers_phones.up.sql
new file mode 100644
index 0000000..440b46a
--- /dev/null
+++ b/migrations/002_create_customers_phones.up.sql
@@ -0,0 +1,7 @@
+create table customers_phones (
+  customer_id VARCHAR(40), 
+  number VARCHAR(20), 
+  valid BOOLEAN, 
+  type integer, 
+  constraint customer_number unique (customer_id, number)
+);
diff --git a/migrations/003_create_customers_addresses.up.sql b/migrations/003_create_customers_addresses.up.sql
new file mode 100644
index 0000000..a6d5431
--- /dev/null
+++ b/migrations/003_create_customers_addresses.up.sql
@@ -0,0 +1,14 @@
+create table customers_addresses(
+  address_id varchar(40) primary key, 
+  customer_id varchar(40), 
+  type SMALLINT, 
+  address1 varchar(120), 
+  address2 varchar(120), 
+  city varchar(50), 
+  state varchar(2), 
+  postal_code varchar(9), 
+  country varchar(3), 
+  validated BOOLEAN, 
+  deleted_at datetime,
+  constraint customer_address unique (customer_id, address1)
+);
diff --git a/migrations/004_create_customer_metadata.up.sql b/migrations/004_create_customer_metadata.up.sql
new file mode 100644
index 0000000..5687202
--- /dev/null
+++ b/migrations/004_create_customer_metadata.up.sql
@@ -0,0 +1,6 @@
+create table customer_metadata(
+  customer_id varchar(40), 
+  meta_key varchar(40), 
+  meta_value varchar(512), 
+  constraint customer_meta_key_val unique (meta_key, meta_value)
+);
diff --git a/migrations/005_create_status_updates.up.sql b/migrations/005_create_status_updates.up.sql
new file mode 100644
index 0000000..8e3d861
--- /dev/null
+++ b/migrations/005_create_status_updates.up.sql
@@ -0,0 +1,6 @@
+create table customer_status_updates(
+  customer_id varchar(40), 
+  future_status integer, 
+  comment varchar(512), 
+  changed_at datetime
+);
diff --git a/migrations/006_create_customer_ofac_searches.up.sql b/migrations/006_create_customer_ofac_searches.up.sql
new file mode 100644
index 0000000..083732c
--- /dev/null
+++ b/migrations/006_create_customer_ofac_searches.up.sql
@@ -0,0 +1,9 @@
+create table customer_ofac_searches(
+  customer_id varchar(40), 
+  entity_id varchar(40), 
+  sdn_name varchar(40), 
+  sdn_type integer, 
+  percentage_match double precision (5, 2), 
+  blocked boolean,
+  created_at datetime
+);
diff --git a/migrations/007_create_customer_ssn.up.sql b/migrations/007_create_customer_ssn.up.sql
new file mode 100644
index 0000000..eb7d221
--- /dev/null
+++ b/migrations/007_create_customer_ssn.up.sql
@@ -0,0 +1,6 @@
+create table customer_ssn(
+  customer_id varchar(40) primary key, 
+  ssn BLOB, 
+  ssn_masked varchar(9), 
+  created_at datetime
+);
diff --git a/migrations/008_create_documents.up.sql b/migrations/008_create_documents.up.sql
new file mode 100644
index 0000000..2863cc9
--- /dev/null
+++ b/migrations/008_create_documents.up.sql
@@ -0,0 +1,8 @@
+create table documents(
+  document_id varchar(40) primary key, 
+  customer_id varchar(40), 
+  type varchar(120), 
+  content_type varchar(40), 
+  uploaded_at datetime,
+  deleted_at datetime
+);
diff --git a/migrations/009_create_disclaimers.up.sql b/migrations/009_create_disclaimers.up.sql
new file mode 100644
index 0000000..072dc57
--- /dev/null
+++ b/migrations/009_create_disclaimers.up.sql
@@ -0,0 +1,7 @@
+create table disclaimers(
+  disclaimer_id varchar(40) primary key, 
+  text text, 
+  document_id varchar(40), 
+  created_at datetime, 
+  deleted_at datetime
+);
diff --git a/migrations/010_create_disclaimer_acceptances.up.sql b/migrations/010_create_disclaimer_acceptances.up.sql
new file mode 100644
index 0000000..a08a5f8
--- /dev/null
+++ b/migrations/010_create_disclaimer_acceptances.up.sql
@@ -0,0 +1,6 @@
+create table if not exists disclaimer_acceptances(
+  disclaimer_id varchar(40), 
+  customer_id varchar(40), 
+  accepted_at datetime, 
+  constraint disclaimer_acceptance_unique_to_customer unique (disclaimer_id, customer_id)
+);
diff --git a/migrations/011_create_accounts.up.sql b/migrations/011_create_accounts.up.sql
new file mode 100644
index 0000000..6c70237
--- /dev/null
+++ b/migrations/011_create_accounts.up.sql
@@ -0,0 +1,16 @@
+create table accounts(
+  account_id varchar(40) primary key, 
+  customer_id varchar(40), 
+  user_id varchar(40), 
+  encrypted_account_number varchar(100), 
+  hashed_account_number varchar(40), 
+  masked_account_number varchar(15), 
+  routing_number varchar(10), 
+  holder_name varchar(60) default '',
+  status varchar(12), 
+  type varchar(12), 
+  created_at datetime, 
+  deleted_at datetime,
+  constraint accounts_unique_to_customer unique (customer_id, hashed_account_number, routing_number)
+);
+
diff --git a/migrations/012_create_validations.up.sql b/migrations/012_create_validations.up.sql
new file mode 100644
index 0000000..79ce0d9
--- /dev/null
+++ b/migrations/012_create_validations.up.sql
@@ -0,0 +1,9 @@
+create table validations(
+  validation_id varchar(40) primary key, 
+  account_id varchar(40), 
+  status varchar(20), 
+  strategy varchar(20), 
+  vendor varchar(20), 
+  created_at datetime, 
+  updated_at datetime
+);
diff --git a/migrations/013_chnage_validations_add_index.up.sql b/migrations/013_chnage_validations_add_index.up.sql
new file mode 100644
index 0000000..fed1f82
--- /dev/null
+++ b/migrations/013_chnage_validations_add_index.up.sql
@@ -0,0 +1,1 @@
+create index idx_validations_validation_account_ids on validations (validation_id, account_id)
diff --git a/migrations/014_create_account_ofac_searches.up.sql b/migrations/014_create_account_ofac_searches.up.sql
new file mode 100644
index 0000000..6daedd5
--- /dev/null
+++ b/migrations/014_create_account_ofac_searches.up.sql
@@ -0,0 +1,9 @@
+create table account_ofac_searches(
+  account_ofac_search_id varchar(40) primary key, 
+  account_id varchar(40), 
+  entity_id varchar(40), 
+  sdn_name varchar(40), 
+  sdn_type integer, 
+  percentage_match double precision (5, 2), 
+  created_at datetime
+);
diff --git a/migrations/015_create_organization_configurations.up.sql b/migrations/015_create_organization_configurations.up.sql
new file mode 100644
index 0000000..6e766ef
--- /dev/null
+++ b/migrations/015_create_organization_configurations.up.sql
@@ -0,0 +1,5 @@
+create table organization_configuration(
+  organization varchar(40) primary key not null, 
+  legal_entity varchar(40) not null, 
+  primary_account varchar(40) not null
+);
diff --git a/pkg/accounts/router.go b/pkg/accounts/router.go
index ea849d4..4dcbbde 100644
--- a/pkg/accounts/router.go
+++ b/pkg/accounts/router.go
@@ -27,7 +27,7 @@ import (
 )
 
 func RegisterRoutes(logger log.Logger, r *mux.Router, accounts Repository, validations validator.Repository, fedClient fed.Client, keeper, transitKeeper *secrets.StringKeeper, validationStrategies map[validator.StrategyKey]validator.Strategy, ofac *AccountOfacSearcher) {
-	logger = logger.WithKeyValue("package", "accounts")
+	logger = logger.Set("package", "accounts")
 
 	r.Methods("GET").Path("/customers/{customerID}/accounts").HandlerFunc(getCustomerAccounts(logger, accounts, fedClient))
 	r.Methods("POST").Path("/customers/{customerID}/accounts").HandlerFunc(createCustomerAccount(logger, accounts, fedClient, keeper, ofac))
@@ -141,18 +141,17 @@ func createCustomerAccount(logger log.Logger, repo Repository, fedClient fed.Cli
 			return
 		}
 		if err := request.validate(); err != nil {
-			logger.LogErrorF("problem validating account: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 		if err := request.disfigure(keeper); err != nil {
-			logger.LogErrorF("problem disfiguring account: %v", err)
+			logger.LogErrorf("problem disfiguring account: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		if _, err := fedClient.LookupInstitution(request.RoutingNumber); err != nil {
-			logger.LogErrorF("problem looking up routing number=%q: %v", request.RoutingNumber, err)
+			logger.LogErrorf("problem looking up routing number=%q: %v", request.RoutingNumber, err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -160,14 +159,14 @@ func createCustomerAccount(logger log.Logger, repo Repository, fedClient fed.Cli
 		customerID, userID := route.GetCustomerID(w, r), moovhttp.GetUserID(r)
 		account, err := repo.CreateCustomerAccount(customerID, userID, &request)
 		if err != nil {
-			logger.LogErrorF("problem saving account: %v", err)
+			logger.LogErrorf("problem saving account: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		// Perform an OFAC search with the Customer information
 		if err := ofac.StoreAccountOFACSearch(account, requestID); err != nil {
-			logger.LogErrorF("error with OFAC search for account=%s: %v", account.AccountID, err)
+			logger.LogErrorf("error with OFAC search for account=%s: %v", account.AccountID, err)
 		}
 
 		w.Header().Set("Content-Type", "application/json; charset=utf-8")
diff --git a/pkg/config/config.go b/pkg/config/config.go
new file mode 100644
index 0000000..01a472f
--- /dev/null
+++ b/pkg/config/config.go
@@ -0,0 +1,49 @@
+package config
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/moov-io/base/database"
+)
+
+type Config struct {
+	Database *database.DatabaseConfig
+}
+
+func New() *Config {
+	return &Config{
+		Database: &database.DatabaseConfig{},
+	}
+}
+
+func (c *Config) Load() error {
+	dbType := strings.ToLower(os.Getenv("DATABASE_TYPE"))
+	switch dbType {
+	case "sqlite", "":
+		path := os.Getenv("SQLITE_DB_PATH")
+		if path == "" || strings.Contains(path, "..") {
+			// set default if empty or trying to escape
+			// don't filepath.ABS to avoid full-fs reads
+			path = "customers.db"
+		}
+
+		c.Database.SQLite = &database.SQLiteConfig{
+			Path: path,
+		}
+
+	case "mysql":
+		c.Database.MySQL = &database.MySQLConfig{
+			Address:  os.Getenv("MYSQL_ADDRESS"),
+			User:     os.Getenv("MYSQL_USER"),
+			Password: os.Getenv("MYSQL_PASSWORD"),
+		}
+		c.Database.DatabaseName = os.Getenv("MYSQL_DATABASE")
+
+	default:
+		return fmt.Errorf("unknown database type: %q", dbType)
+	}
+
+	return nil
+}
diff --git a/pkg/configuration/router.go b/pkg/configuration/router.go
index 6458b00..022320b 100644
--- a/pkg/configuration/router.go
+++ b/pkg/configuration/router.go
@@ -53,7 +53,7 @@ func listAllowedContentTypes() string {
 }
 
 func RegisterRoutes(logger log.Logger, r *mux.Router, repo Repository, bucketFunc storage.BucketFunc) {
-	logger = logger.WithKeyValue("package", "configuration")
+	logger = logger.Set("package", "configuration")
 
 	r.Methods("GET").Path("/configuration/customers").HandlerFunc(getOrganizationConfig(logger, repo))
 	r.Methods("PUT").Path("/configuration/customers").HandlerFunc(updateOrganizationConfig(logger, repo))
@@ -111,11 +111,11 @@ func getOrganizationLogo(logger log.Logger, repo Repository, bucketFactory stora
 			return
 		}
 
-		logger = logger.WithKeyValue("organization", organization)
+		logger = logger.Set("organization", organization)
 
 		bucket, err := bucketFactory()
 		if err != nil {
-			logger.LogError("problem retrieving logo image", err)
+			logger.LogErrorf("problem retrieving logo image: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -123,24 +123,20 @@ func getOrganizationLogo(logger log.Logger, repo Repository, bucketFactory stora
 
 		rdr, err := bucket.NewReader(r.Context(), makeDocumentKey(organization), nil)
 		if err != nil {
-			msg := "error retrieving logo file"
 			if gcerrors.Code(err) == gcerrors.NotFound {
-				msg = msg + " - file not found"
-				logger.LogError(msg, err)
+				logger.LogErrorf("logo file not found: %v", err)
 				http.NotFound(w, r)
 				return
 			}
 
-			logger.LogError(msg, err)
-			moovhttp.Problem(w, fmt.Errorf(msg))
+			moovhttp.Problem(w, logger.LogErrorf("error retrieving logo file: %v", err).Err())
 			return
 		}
 		defer rdr.Close()
 
 		fBytes, err := ioutil.ReadAll(rdr)
 		if err != nil || fBytes == nil {
-			logger.LogError("problem reading logo file", err)
-			moovhttp.Problem(w, fmt.Errorf("problem reading logo file - error=%v", err))
+			moovhttp.Problem(w, logger.LogErrorf("problem reading logo file: %v", err).Err())
 			return
 		}
 
@@ -160,7 +156,7 @@ func uploadOrganizationLogo(logger log.Logger, repo Repository, bucketFactory st
 
 		file, _, err := r.FormFile("file")
 		if file == nil || err != nil {
-			logger.LogError(errMissingFile.Error(), err)
+			logger.LogErrorf("%s: %v", errMissingFile.Error(), err)
 			moovhttp.Problem(w, errMissingFile)
 			return
 		}
@@ -169,14 +165,14 @@ func uploadOrganizationLogo(logger log.Logger, repo Repository, bucketFactory st
 		// Detect the content type by reading the first 512 bytes of r.Body (read into file as we expect a multipart request)
 		buf := make([]byte, 512)
 		if _, err := file.Read(buf); err != nil && err != io.EOF {
-			logger.LogError("problem reading file", err)
+			logger.LogErrorf("problem reading file: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		contentType := http.DetectContentType(buf)
 		if !allowedContentTypes[contentType] {
-			logger.WithKeyValue("contentType", contentType).
+			logger.Set("contentType", contentType).
 				Log("unsupported content type for logo image file")
 			moovhttp.Problem(w, errUnsupportedType)
 			return
@@ -184,7 +180,7 @@ func uploadOrganizationLogo(logger log.Logger, repo Repository, bucketFactory st
 
 		bucket, err := bucketFactory()
 		if err != nil {
-			logger.LogError("problem uploading logo image", err)
+			logger.LogErrorf("problem uploading logo image: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -198,7 +194,7 @@ func uploadOrganizationLogo(logger log.Logger, repo Repository, bucketFactory st
 			ContentType:        contentType,
 		})
 		if err != nil {
-			logger.LogError("problem uploading logo image", err)
+			logger.LogErrorf("problem uploading logo image: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -206,7 +202,7 @@ func uploadOrganizationLogo(logger log.Logger, repo Repository, bucketFactory st
 
 		written, err := io.Copy(writer, io.LimitReader(io.MultiReader(bytes.NewReader(buf), file), maxImageSize))
 		if err != nil || written == 0 {
-			logger.LogError("problem uploading logo image", err)
+			logger.LogErrorf("problem uploading logo image: %v", err)
 			moovhttp.Problem(w, fmt.Errorf("problem writing file - wrote %d bytes with error=%v", written, err))
 			return
 		}
diff --git a/pkg/customers/address.go b/pkg/customers/address.go
index dcf32db..b93e8a4 100644
--- a/pkg/customers/address.go
+++ b/pkg/customers/address.go
@@ -19,7 +19,7 @@ var (
 )
 
 func AddCustomerAddressRoutes(logger log.Logger, r *mux.Router, repo CustomerRepository) {
-	logger = logger.WithKeyValue("package", "customers")
+	logger = logger.Set("package", "customers")
 
 	r.Methods("POST").Path("/customers/{customerID}/addresses").HandlerFunc(createCustomerAddress(logger, repo))
 	r.Methods("PUT").Path("/customers/{customerID}/addresses/{addressID}").HandlerFunc(updateCustomerAddress(logger, repo))
@@ -77,7 +77,7 @@ func createCustomerAddress(logger log.Logger, repo CustomerRepository) http.Hand
 			return
 		}
 
-		logger.Log(fmt.Sprintf("added address for customer=%s", customerID))
+		logger.Logf("added address for customer=%s", customerID)
 		respondWithCustomer(logger, w, customerID, organization, requestID, repo)
 	}
 }
@@ -124,12 +124,12 @@ func updateCustomerAddress(logger log.Logger, repo CustomerRepository) http.Hand
 		}
 
 		if err := repo.updateCustomerAddress(customerID, addressId, req); err != nil {
-			logger.LogErrorF("error updating customer's address: customer=%s address=%s: %v", customerID, addressId, err)
+			logger.LogErrorf("error updating customer's address: customer=%s address=%s: %v", customerID, addressId, err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
-		logger.Log(fmt.Sprintf("updating address=%s for customer=%s", addressId, customerID))
+		logger.Logf("updating address=%s for customer=%s", addressId, customerID)
 
 		respondWithCustomer(logger, w, customerID, organization, requestID, repo)
 	}
@@ -146,12 +146,12 @@ func deleteCustomerAddress(logger log.Logger, repo CustomerRepository) http.Hand
 
 		err := repo.deleteCustomerAddress(customerID, addressId)
 		if err != nil {
-			logger.LogErrorF("error deleting customer's address: customer=%s address=%s: %v", customerID, addressId, err)
+			logger.LogErrorf("error deleting customer's address: customer=%s address=%s: %v", customerID, addressId, err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
-		logger.Log(fmt.Sprintf("successfully deleted address=%s for customer=%s", addressId, customerID))
+		logger.Logf("successfully deleted address=%s for customer=%s", addressId, customerID)
 
 		w.WriteHeader(http.StatusNoContent)
 	}
diff --git a/pkg/customers/approval_ofac.go b/pkg/customers/approval_ofac.go
index ad8df77..9b56acb 100644
--- a/pkg/customers/approval_ofac.go
+++ b/pkg/customers/approval_ofac.go
@@ -98,7 +98,7 @@ func (s *OFACSearcher) storeCustomerOFACSearch(cust *client.Customer, requestID
 }
 
 func AddOFACRoutes(logger log.Logger, r *mux.Router, repo CustomerRepository, ofac *OFACSearcher) {
-	logger = logger.WithKeyValue("package", "customers")
+	logger = logger.Set("package", "customers")
 
 	r.Methods("GET").Path("/customers/{customerID}/ofac").HandlerFunc(getLatestCustomerOFACSearch(logger, repo))
 	r.Methods("PUT").Path("/customers/{customerID}/refresh/ofac").HandlerFunc(refreshOFACSearch(logger, repo, ofac))
@@ -152,26 +152,26 @@ func refreshOFACSearch(logger log.Logger, repo CustomerRepository, ofac *OFACSea
 			return
 		}
 
-		logger.Log(fmt.Sprintf("running live OFAC search for customer=%s", customerID))
+		logger.Logf("running live OFAC search for customer=%s", customerID)
 
 		if err := ofac.storeCustomerOFACSearch(cust, requestID); err != nil {
-			logger.LogErrorF("error refreshing ofac search: %v", err)
+			logger.LogErrorf("error refreshing ofac search: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		result, err := repo.getLatestCustomerOFACSearch(customerID, organization)
 		if err != nil {
-			logger.LogErrorF("error getting latest ofac search: %v", err)
+			logger.LogErrorf("error getting latest ofac search: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		if result.Blocked {
-			logger.LogErrorF("customer=%s matched against OFAC entity=%s with a score of %.2f - rejecting customer", cust.CustomerID, result.EntityID, result.Match)
+			logger.LogErrorf("customer=%s matched against OFAC entity=%s with a score of %.2f - rejecting customer", cust.CustomerID, result.EntityID, result.Match)
 
 			if err := repo.updateCustomerStatus(cust.CustomerID, client.CUSTOMERSTATUS_REJECTED, "manual OFAC refresh"); err != nil {
-				logger.LogErrorF("error updating customer=%s error=%v", cust.CustomerID, err)
+				logger.LogErrorf("error updating customer=%s error=%v", cust.CustomerID, err)
 				moovhttp.Problem(w, err)
 				return
 			}
diff --git a/pkg/customers/customer_search.go b/pkg/customers/customer_search.go
index 45e8824..6caac5d 100644
--- a/pkg/customers/customer_search.go
+++ b/pkg/customers/customer_search.go
@@ -41,7 +41,7 @@ func searchCustomers(logger log.Logger, repo CustomerRepository) http.HandlerFun
 			return
 		}
 
-		logger.Log(fmt.Sprintf("found %d customers in search", len(customers)))
+		logger.Logf("found %d customers in search", len(customers))
 
 		w.Header().Set("Content-Type", "application/json; charset=utf-8")
 		w.WriteHeader(http.StatusOK)
diff --git a/pkg/customers/customers.go b/pkg/customers/customers.go
index 33aa48a..ee0da13 100644
--- a/pkg/customers/customers.go
+++ b/pkg/customers/customers.go
@@ -27,7 +27,7 @@ import (
 )
 
 func AddCustomerRoutes(logger log.Logger, r *mux.Router, repo CustomerRepository, customerSSNStorage *ssnStorage, ofac *OFACSearcher) {
-	logger = logger.WithKeyValue("package", "customers")
+	logger = logger.Set("package", "customers")
 
 	r.Methods("GET").Path("/customers").HandlerFunc(searchCustomers(logger, repo))
 	r.Methods("GET").Path("/customers/{customerID}").HandlerFunc(getCustomer(logger, repo))
@@ -95,7 +95,7 @@ func deleteCustomer(logger log.Logger, repo CustomerRepository) func(http.Respon
 func respondWithCustomer(logger log.Logger, w http.ResponseWriter, customerID, organization string, requestID string, repo CustomerRepository) {
 	cust, err := repo.GetCustomer(customerID, organization)
 	if err != nil {
-		logger.LogErrorF("getCustomer: lookup: %v", err)
+		logger.LogErrorf("getCustomer: lookup: %v", err)
 		moovhttp.Problem(w, err)
 		return
 	}
@@ -280,42 +280,42 @@ func createCustomer(logger log.Logger, repo CustomerRepository, customerSSNStora
 			return
 		}
 		if err := req.validate(); err != nil {
-			logger.LogError("error validating new customer", err)
+			logger.LogErrorf("error validating new customer: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		cust, ssn, err := req.asCustomer(customerSSNStorage)
 		if err != nil {
-			logger.LogErrorF("problem transforming request into Customer=%s: %v", cust.CustomerID, err)
+			logger.LogErrorf("problem transforming request into Customer=%s: %v", cust.CustomerID, err)
 			moovhttp.Problem(w, err)
 			return
 		}
 		if ssn != nil {
 			err := customerSSNStorage.repo.saveCustomerSSN(ssn)
 			if err != nil {
-				logger.LogErrorF("problem saving SSN for Customer=%s: %v", cust.CustomerID, err)
+				logger.LogErrorf("problem saving SSN for Customer=%s: %v", cust.CustomerID, err)
 				moovhttp.Problem(w, fmt.Errorf("saveCustomerSSN: %v", err))
 				return
 			}
 		}
 		if err := repo.CreateCustomer(cust, organization); err != nil {
-			logger.LogErrorF("createCustomer: %v", err)
+			logger.LogErrorf("createCustomer: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 		if err := repo.replaceCustomerMetadata(cust.CustomerID, cust.Metadata); err != nil {
-			logger.LogErrorF("updating metadata for customer=%s failed: %v", cust.CustomerID, err)
+			logger.LogErrorf("updating metadata for customer=%s failed: %v", cust.CustomerID, err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		// Perform an OFAC search with the Customer information
 		if err := ofac.storeCustomerOFACSearch(cust, requestID); err != nil {
-			logger.LogErrorF("error with OFAC search for customer=%s: %v", cust.CustomerID, err)
+			logger.LogErrorf("error with OFAC search for customer=%s: %v", cust.CustomerID, err)
 		}
 
-		logger.Log(fmt.Sprintf("created customer=%s", cust.CustomerID))
+		logger.Logf("created customer=%s", cust.CustomerID)
 
 		cust, err = repo.GetCustomer(cust.CustomerID, organization)
 		if err != nil {
@@ -349,38 +349,38 @@ func updateCustomer(logger log.Logger, repo CustomerRepository, customerSSNStora
 			return
 		}
 		if err := req.validate(); err != nil {
-			logger.LogError("error validating customer payload", err)
+			logger.LogErrorf("error validating customer payload: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		cust, ssn, err := req.asCustomer(customerSSNStorage)
 		if err != nil {
-			logger.LogErrorF("transforming request into Customer=%s: %v", cust.CustomerID, err)
+			logger.LogErrorf("transforming request into Customer=%s: %v", cust.CustomerID, err)
 			moovhttp.Problem(w, err)
 			return
 		}
 		if ssn != nil {
 			err := customerSSNStorage.repo.saveCustomerSSN(ssn)
 			if err != nil {
-				logger.LogErrorF("error saving SSN for Customer=%s: %v", cust.CustomerID, err)
+				logger.LogErrorf("error saving SSN for Customer=%s: %v", cust.CustomerID, err)
 				moovhttp.Problem(w, fmt.Errorf("saving customer's SSN: %v", err))
 				return
 			}
 		}
 		if err := repo.updateCustomer(cust, organization); err != nil {
-			logger.LogErrorF("error updating customer: %v", err)
+			logger.LogErrorf("error updating customer: %v", err)
 			moovhttp.Problem(w, fmt.Errorf("updating customer: %v", err))
 			return
 		}
 
 		if err := repo.replaceCustomerMetadata(cust.CustomerID, cust.Metadata); err != nil {
-			logger.LogErrorF("error updating metadata for customer=%s: %v", cust.CustomerID, err)
+			logger.LogErrorf("error updating metadata for customer=%s: %v", cust.CustomerID, err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
-		logger.Log(fmt.Sprintf("updated customer=%s", cust.CustomerID))
+		logger.Logf("updated customer=%s", cust.CustomerID)
 		cust, err = repo.GetCustomer(cust.CustomerID, organization)
 		if err != nil {
 			moovhttp.Problem(w, err)
diff --git a/pkg/documents/documents.go b/pkg/documents/documents.go
index 47e8d5d..bec08c7 100644
--- a/pkg/documents/documents.go
+++ b/pkg/documents/documents.go
@@ -46,7 +46,7 @@ const (
 )
 
 func AddDocumentRoutes(logger log.Logger, r *mux.Router, repo DocumentRepository, keeper *secrets.Keeper, bucketFactory storage.BucketFunc) {
-	logger = logger.WithKeyValue("package", "documents")
+	logger = logger.Set("package", "documents")
 
 	r.Methods("GET").Path("/customers/{customerID}/documents").HandlerFunc(getCustomerDocuments(logger, repo))
 	r.Methods("POST").Path("/customers/{customerID}/documents").HandlerFunc(uploadCustomerDocument(logger, repo, keeper, bucketFactory))
@@ -74,7 +74,7 @@ func getCustomerDocuments(logger log.Logger, repo DocumentRepository) http.Handl
 
 		docs, err := repo.getCustomerDocuments(customerID, organization)
 		if err != nil {
-			logger.WithKeyValue("customerID", customerID).LogError("failed to get customer document", err)
+			logger.Set("customerID", customerID).LogErrorf("failed to get customer document: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -106,7 +106,7 @@ func uploadCustomerDocument(logger log.Logger, repo DocumentRepository, keeper *
 			return
 		}
 
-		logger = logger.WithKeyValue("customerID", customerID)
+		logger = logger.Set("customerID", customerID)
 
 		documentType, err := readDocumentType(r.URL.Query().Get("type"))
 		if err != nil {
@@ -119,18 +119,18 @@ func uploadCustomerDocument(logger log.Logger, repo DocumentRepository, keeper *
 		file, fileHeader, err := r.FormFile("file")
 		if err != nil {
 			if strings.Contains(err.Error(), "request body too large") {
-				logger.LogError("max form size exceeded", err)
+				logger.LogErrorf("max form size exceeded: %v", err)
 				moovhttp.Problem(w, fmt.Errorf("request body exceeds maximum size of %s", maxFormSize))
 				return
 			}
-			logger.LogError("error reading form file", err)
+			logger.LogErrorf("error reading form file: %v", err)
 			moovhttp.Problem(w, fmt.Errorf("expected multipart upload with key of 'file' error=%v", err))
 			return
 		}
 		defer file.Close()
 
 		if fileHeader.Size > int64(maxDocumentSize) {
-			logger.LogErrorF("failed to peek: %v", err)
+			logger.LogErrorf("failed to peek: %v", err)
 			moovhttp.Problem(w, fmt.Errorf("file exceeds maximum size of %s", maxDocumentSize))
 			return
 		}
@@ -138,7 +138,7 @@ func uploadCustomerDocument(logger log.Logger, repo DocumentRepository, keeper *
 		fileReader := bufio.NewReader(file)
 		sniff, err := fileReader.Peek(512)
 		if err != nil && err != io.EOF {
-			logger.LogError("peek failed", err)
+			logger.LogErrorf("peek failed: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -147,7 +147,7 @@ func uploadCustomerDocument(logger log.Logger, repo DocumentRepository, keeper *
 		// Grab our cloud bucket before writing into our database
 		bucket, err := bucketFactory()
 		if err != nil {
-			logger.LogError("failed to create bucket", err)
+			logger.LogErrorf("failed to create bucket: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -160,11 +160,11 @@ func uploadCustomerDocument(logger log.Logger, repo DocumentRepository, keeper *
 			UploadedAt:  time.Now(),
 		}
 		if err := repo.writeCustomerDocument(customerID, doc); err != nil {
-			logger.LogError("failed to write customer document", err)
+			logger.LogErrorf("failed to write customer document: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
-		logger.Log(fmt.Sprintf("uploading document=%s (content-type: %s)", doc.DocumentID, contentType))
+		logger.Logf("uploading document=%s (content-type: %s)", doc.DocumentID, contentType)
 
 		// Write our document from the request body
 		ctx, cancelFn := context.WithTimeout(context.TODO(), 60*time.Second)
@@ -172,26 +172,26 @@ func uploadCustomerDocument(logger log.Logger, repo DocumentRepository, keeper *
 
 		fBytes, err := ioutil.ReadAll(fileReader)
 		if err != nil {
-			logger.LogError("read failed", err)
+			logger.LogErrorf("read failed: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 		encryptedDoc, err := keeper.Encrypt(ctx, fBytes)
 		if err != nil {
-			logger.LogError("failed to encrypt document", err)
+			logger.LogErrorf("failed to encrypt document: %v", err)
 			moovhttp.Problem(w, fmt.Errorf("file upload error - %v", err))
 			return
 		}
 
 		documentKey := makeDocumentKey(customerID, doc.DocumentID)
-		logger.Log(fmt.Sprintf("writing %s", documentKey))
+		logger.Logf("writing %s", documentKey)
 
 		err = bucket.WriteAll(ctx, documentKey, encryptedDoc, &blob.WriterOptions{
 			ContentDisposition: "inline",
 			ContentType:        contentType,
 		})
 		if err != nil {
-			logger.LogError("problem uploading document", err)
+			logger.LogErrorf("problem uploading document: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -215,13 +215,13 @@ func retrieveRawDocument(logger log.Logger, repo DocumentRepository, keeper *sec
 			return
 		}
 
-		logger = logger.WithKeyValue("customerID", customerID).
-			WithKeyValue("documentID", documentID)
+		logger = logger.Set("customerID", customerID).
+			Set("documentID", documentID)
 
 		// reject the request if the document is deleted
 		if exists, err := repo.exists(customerID, documentID, organization); !exists || err != nil {
 			if err != nil {
-				logger.LogError("failed to check document existence", err)
+				logger.LogErrorf("failed to check document existence: %v", err)
 			}
 			http.NotFound(w, r)
 			return
@@ -247,14 +247,14 @@ func retrieveRawDocument(logger log.Logger, repo DocumentRepository, keeper *sec
 
 		encryptedDoc, err := ioutil.ReadAll(rdr)
 		if err != nil {
-			logger.LogError("failed reading document from storage bucket", err)
+			logger.LogErrorf("failed reading document from storage bucket: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
 
 		doc, err := keeper.Decrypt(ctx, encryptedDoc)
 		if err != nil {
-			logger.LogError("failed to decrypt document", err)
+			logger.LogErrorf("failed to decrypt document: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -277,12 +277,12 @@ func deleteCustomerDocument(logger log.Logger, repo DocumentRepository) http.Han
 			return
 		}
 
-		logger = logger.WithKeyValue("customerID", customerID).
-			WithKeyValue("documentID", documentID)
+		logger = logger.Set("customerID", customerID).
+			Set("documentID", documentID)
 
 		err := repo.deleteCustomerDocument(customerID, documentID)
 		if err != nil {
-			logger.LogError("failed to delete document", err)
+			logger.LogErrorf("failed to delete document: %v", err)
 			moovhttp.Problem(w, fmt.Errorf("failed to %v", err))
 			return
 		}
diff --git a/pkg/documents/storage/bucket.go b/pkg/documents/storage/bucket.go
index 508cee1..2aef326 100644
--- a/pkg/documents/storage/bucket.go
+++ b/pkg/documents/storage/bucket.go
@@ -27,7 +27,7 @@ import (
 type BucketFunc func() (*blob.Bucket, error)
 
 func GetBucket(logger log.Logger, bucketName, cloudProvider string, FileblobSigner *fileblob.URLSignerHMAC) BucketFunc {
-	logger = logger.WithKeyValue("package", "storage")
+	logger = logger.Set("package", "storage")
 
 	return func() (*blob.Bucket, error) {
 		ctx, cancelFn := context.WithTimeout(context.TODO(), 10*time.Second)
@@ -68,10 +68,10 @@ func awsBucket(ctx context.Context, logger log.Logger, bucketName string) (*blob
 
 	bucket, err := s3blob.OpenBucket(ctx, s, bucketName, nil)
 	if err != nil {
-		logger.LogErrorF("ERROR creating %s aws bucket: %v", bucketName, err)
+		logger.LogErrorf("ERROR creating %s aws bucket: %v", bucketName, err)
 		return nil, err
 	}
-	logger.Log(fmt.Sprintf("created %s aws bucket: %T", bucketName, bucket))
+	logger.Logf("created %s aws bucket: %T", bucketName, bucket)
 	return bucket, nil
 }
 
@@ -87,7 +87,7 @@ func fileBucket(ctx context.Context, logger log.Logger, bucketName string, signe
 	if err := os.Mkdir(bucketName, 0777); strings.Contains(bucketName, "..") || (err != nil && !os.IsExist(err)) {
 		return nil, fmt.Errorf("problem creating %s error=%v", bucketName, err)
 	}
-	logger.LogErrorF("created %s for file bucket", bucketName)
+	logger.LogErrorf("created %s for file bucket", bucketName)
 	return fileblob.OpenBucket(bucketName, &fileblob.Options{
 		URLSigner: signer,
 	})
@@ -107,9 +107,9 @@ func gcpBucket(ctx context.Context, logger log.Logger, bucketName string) (*blob
 
 	bucket, err := gcsblob.OpenBucket(ctx, c, bucketName, nil)
 	if err != nil {
-		logger.LogErrorF("ERROR creating %s gcp bucket: %v", bucketName, err)
+		logger.LogErrorf("ERROR creating %s gcp bucket: %v", bucketName, err)
 		return nil, err
 	}
-	logger.Log(fmt.Sprintf("created %s gcp bucket: %v", bucketName, err))
+	logger.Logf("created %s gcp bucket: %v", bucketName, err)
 	return bucket, nil
 }
diff --git a/pkg/documents/storage/files.go b/pkg/documents/storage/files.go
index b322984..bed9865 100644
--- a/pkg/documents/storage/files.go
+++ b/pkg/documents/storage/files.go
@@ -22,7 +22,7 @@ import (
 )
 
 func AddFileblobRoutes(logger log.Logger, r *mux.Router, signer *fileblob.URLSignerHMAC, bucketFactory BucketFunc) {
-	logger = logger.WithKeyValue("package", "storage")
+	logger = logger.Set("package", "storage")
 
 	if v := os.Getenv("FILEBLOB_BASE_URL"); v != "" {
 		u, err := url.Parse(v)
@@ -62,7 +62,7 @@ func proxyLocalFile(logger log.Logger, signer *fileblob.URLSignerHMAC, bucketFac
 		}
 		defer rdr.Close()
 
-		logger.Log(fmt.Sprintf("proxying document=%s contentType=%s", key, rdr.ContentType()))
+		logger.Logf("proxying document=%s contentType=%s", key, rdr.ContentType())
 
 		w.Header().Set("Content-Disposition", "inline")
 		w.Header().Set("Content-Length", fmt.Sprintf("%d", rdr.Size()))
diff --git a/pkg/fed/client.go b/pkg/fed/client.go
index 51a7ae3..abbde8f 100644
--- a/pkg/fed/client.go
+++ b/pkg/fed/client.go
@@ -105,8 +105,8 @@ func NewClient(logger log.Logger, endpoint string, debug bool) Client {
 		}
 	}
 
-	logger = logger.WithKeyValue("package", "fed")
-	logger.Log(fmt.Sprintf("using %s for FED address", conf.BasePath))
+	logger = logger.Set("package", "fed")
+	logger.Logf("using %s for FED address", conf.BasePath)
 
 	return &moovClient{
 		underlying: moovfed.NewAPIClient(conf),
diff --git a/pkg/paygate/client.go b/pkg/paygate/client.go
index 812fa36..ae305ec 100644
--- a/pkg/paygate/client.go
+++ b/pkg/paygate/client.go
@@ -98,7 +98,7 @@ func (c *moovClient) InitiateMicroDeposits(userID string, destination client.Des
 		return fmt.Errorf("unexpected HTTP status: %s", resp.Status)
 	}
 
-	c.logger.Log(fmt.Sprintf("created microDepositID=%s for accountID=%s", micro.MicroDepositID, destination.AccountID))
+	c.logger.Logf("created microDepositID=%s for accountID=%s", micro.MicroDepositID, destination.AccountID)
 
 	return nil
 }
@@ -124,8 +124,8 @@ func NewClient(logger log.Logger, endpoint string, debug bool) Client {
 		}
 	}
 
-	logger = logger.WithKeyValue("package", "paygate")
-	logger.Log(fmt.Sprintf("using %s for PayGate address", conf.BasePath))
+	logger = logger.Set("package", "paygate")
+	logger.Logf("using %s for PayGate address", conf.BasePath)
 
 	return &moovClient{
 		logger:     logger,
diff --git a/pkg/reports/router.go b/pkg/reports/router.go
index e25e407..e229f1c 100644
--- a/pkg/reports/router.go
+++ b/pkg/reports/router.go
@@ -22,7 +22,7 @@ func AddRoutes(
 	customerRepo customers.CustomerRepository,
 	accountRepo accounts.Repository,
 ) {
-	logger = logger.WithKeyValue("package", "reports")
+	logger = logger.Set("package", "reports")
 
 	r.Methods("GET").Path("/reports/accounts").HandlerFunc(getCustomerAccounts(logger, customerRepo, accountRepo))
 }
@@ -56,7 +56,7 @@ func getCustomerAccounts(
 
 		allAccounts, err := accountRepo.GetCustomerAccountsByIDs(accountIDs)
 		if err != nil {
-			logger.LogError("error getting customers' accounts", err)
+			logger.LogErrorf("error getting customers' accounts: %v", err)
 			moovhttp.Problem(w, err)
 			return
 		}
@@ -65,7 +65,7 @@ func getCustomerAccounts(
 		for _, acc := range allAccounts {
 			cust, err := customerRepo.GetCustomer(acc.CustomerID, organization)
 			if err != nil {
-				logger.LogError("error getting customer", err)
+				logger.LogErrorf("error getting customer: %v", err)
 				moovhttp.Problem(w, err)
 				return
 			}
diff --git a/pkg/route/response.go b/pkg/route/response.go
index fae61a8..82f8a22 100644
--- a/pkg/route/response.go
+++ b/pkg/route/response.go
@@ -52,7 +52,7 @@ func (w *ResponseWriter) WriteHeader(code int) {
 	}
 
 	if requestID := GetRequestID(w.request); requestID != "" && w.log != nil {
-		w.log.WithMap(map[string]string{
+		w.log.With(log.Fields{
 			"method":    w.request.Method,
 			"path":      w.request.URL.Path,
 			"status":    strconv.Itoa(code),
diff --git a/pkg/watchman/watchman.go b/pkg/watchman/watchman.go
index 94d9958..31babdc 100644
--- a/pkg/watchman/watchman.go
+++ b/pkg/watchman/watchman.go
@@ -140,13 +140,13 @@ func (c *moovWatchmanClient) Search(ctx context.Context, name string, requestID
 	// Take an Alt and find the SDN for it if that was the highest match
 	if len(search.SDNs) == 0 && len(search.AltNames) > 0 {
 		alt := search.AltNames[0]
-		c.logger.Log(fmt.Sprintf("Found AltName=%s,SDN=%s with no higher matched SDNs", alt.AlternateID, alt.EntityID))
+		c.logger.Logf("Found AltName=%s,SDN=%s with no higher matched SDNs", alt.AlternateID, alt.EntityID)
 		return c.altToSDN(ctx, search.AltNames[0], requestID)
 	}
 	// AltName matched higher than SDN names, so return the SDN of the matched AltName
 	if len(search.SDNs) > 0 && len(search.AltNames) > 0 && (search.AltNames[0].Match > 0.1) && search.AltNames[0].Match > search.SDNs[0].Match {
 		alt := search.AltNames[0]
-		c.logger.Log(fmt.Sprintf("AltName=%s,SDN=%s had higher match than SDN=%s", alt.AlternateID, alt.EntityID, search.SDNs[0].EntityID))
+		c.logger.Logf("AltName=%s,SDN=%s had higher match than SDN=%s", alt.AlternateID, alt.EntityID, search.SDNs[0].EntityID)
 		return c.altToSDN(ctx, alt, requestID)
 	}
 	// Return the SDN as Alts matched lower
@@ -174,8 +174,8 @@ func NewClient(logger log.Logger, endpoint string, debug bool) Client {
 		conf.BasePath = endpoint // override from provided Watchman_ENDPOINT env variable
 	}
 
-	logger = logger.WithKeyValue("package", "watchman")
-	logger.Log(fmt.Sprintf("using %s for Watchman address", conf.BasePath))
+	logger = logger.Set("package", "watchman")
+	logger.Logf("using %s for Watchman address", conf.BasePath)
 
 	return &moovWatchmanClient{
 		underlying: watchman.NewAPIClient(conf),


Test patch:
diff --git a/internal/database/database_test.go b/internal/database/database_test.go
deleted file mode 100644
index 0c40674..0000000
--- a/internal/database/database_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2020 The Moov Authors
-// Use of this source code is governed by an Apache License
-// license that can be found in the LICENSE file.
-
-package database
-
-import (
-	"errors"
-	"testing"
-
-	"github.com/moov-io/base/log"
-)
-
-func TestNewDataBase__MYSQL(t *testing.T) {
-	_, err := New(log.NewNopLogger(), "Mysql")
-	if err == nil {
-		t.Error("Error: expected access denied for use")
-	}
-}
-
-func TestNewDataBase__invalid_type(t *testing.T) {
-	_, err := New(log.NewNopLogger(), "db2")
-	if err == nil {
-		t.Errorf("Error: expected unknown database type %s", "db2")
-	}
-}
-
-func TestUniqueViolation(t *testing.T) {
-	err := errors.New(`problem upserting depository="282f6ffcd9ba5b029afbf2b739ee826e22d9df3b", userId="f25f48968da47ef1adb5b6531a1c2197295678ce": Error 1062: Duplicate entry '282f6ffcd9ba5b029afbf2b739ee826e22d9df3b' for key 'PRIMARY'`)
-	if !UniqueViolation(err) {
-		t.Error("should have matched unique violation")
-	}
-
-	err = errors.New(`problem upserting depository="7d676c65eccd48090ff238a0d5e35eb6126c23f2", userId="80cfe1311d9eb7659d02cba9ee6cb04ed3739a85": UNIQUE constraint failed: depositories.depository_id`)
-	if !UniqueViolation(err) {
-		t.Error("should have matched unique violation")
-	}
-}
diff --git a/internal/database/mysql_test.go b/internal/database/mysql_test.go
deleted file mode 100644
index de521dd..0000000
--- a/internal/database/mysql_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2020 The Moov Authors
-// Use of this source code is governed by an Apache License
-// license that can be found in the LICENSE file.
-
-package database
-
-import (
-	"errors"
-	"testing"
-
-	"github.com/moov-io/base/log"
-)
-
-func TestMySQL__basic(t *testing.T) {
-	db := CreateTestMySQLDB(t)
-	defer db.Close()
-
-	if err := db.DB.Ping(); err != nil {
-		t.Fatal(err)
-	}
-
-	// create a phony MySQL
-	m := mysqlConnection(log.NewNopLogger(), "user", "pass", "127.0.0.1:3006", "db")
-
-	conn, err := m.Connect()
-	defer func() {
-		if conn != nil {
-			conn.Close()
-		}
-	}()
-	if conn != nil || err == nil {
-		t.Fatalf("conn=%#v expected error", conn)
-	}
-}
-
-func TestMySQLUniqueViolation(t *testing.T) {
-	err := errors.New(`problem upserting customer="7d676c65eccd48090ff238a0d5e35eb6126c23f2", first_name="John", middle_name="B", last_name="Doe": Error 1062: Duplicate entry '282f6ffcd9ba5b029afbf2b739ee826e22d9df3b' for key 'PRIMARY'`)
-	if !UniqueViolation(err) {
-		t.Error("should have matched unique violation")
-	}
-}
diff --git a/internal/database/sqlite_test.go b/internal/database/sqlite_test.go
deleted file mode 100644
index 3126584..0000000
--- a/internal/database/sqlite_test.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2020 The Moov Authors
-// Use of this source code is governed by an Apache License
-// license that can be found in the LICENSE file.
-
-package database
-
-import (
-	"errors"
-	"strings"
-	"testing"
-
-	"github.com/moov-io/base/log"
-)
-
-func TestSQLite__basic(t *testing.T) {
-	db := CreateTestSqliteDB(t)
-	defer db.Close()
-
-	if err := db.DB.Ping(); err != nil {
-		t.Fatal(err)
-	}
-
-	// error case
-	s := sqliteConnection(log.NewNopLogger(), "/tmp/path/doesnt/exist")
-
-	conn, err := s.Connect()
-
-	defer func(t *testing.T) {
-		if err := conn.Close(); err != nil {
-			t.Errorf("close: %v", err)
-		}
-	}(t)
-
-	if err := conn.Ping(); err == nil {
-		t.Fatal("expected error")
-	}
-	if err == nil {
-		t.Fatalf("conn=%#v expected error", conn)
-	}
-}
-
-func TestSqlite__getSqlitePath(t *testing.T) {
-	if v := getSqlitePath(); v != "customers.db" {
-		t.Errorf("got %s", v)
-	}
-}
-
-func TestSqliteUniqueViolation(t *testing.T) {
-	err := errors.New(`problem upserting customer="7d676c65eccd48090ff238a0d5e35eb6126c23f2", first_name="John", middle_name="B", last_name="Doe": : UNIQUE constraint failed: customer.customer_id`)
-	if !UniqueViolation(err) {
-		t.Error("should have matched unique violation")
-	}
-}
-
-// TestSQLite__SERR validates sqliteConnection doesn't contain an error
-func TestSQLite__SERR(t *testing.T) {
-	// error case
-	s := sqliteConnection(log.NewNopLogger(), "/tmp/path/doesnt/exist")
-
-	s.err = errors.New(": invalid")
-	_, err := s.Connect()
-
-	if err != nil {
-		if !strings.Contains(err.Error(), ": invalid") {
-			t.Fatal("Expected invalid")
-		}
-	}
-}
diff --git a/pkg/accounts/repository_test.go b/pkg/accounts/repository_test.go
index 2482072..ae23288 100644
--- a/pkg/accounts/repository_test.go
+++ b/pkg/accounts/repository_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/moov-io/base"
 	"github.com/stretchr/testify/require"
 
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 	"github.com/moov-io/customers/pkg/client"
 	"github.com/moov-io/customers/pkg/customers"
 	"github.com/moov-io/customers/pkg/secrets"
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
new file mode 100644
index 0000000..1a6b19a
--- /dev/null
+++ b/pkg/config/config_test.go
@@ -0,0 +1,57 @@
+package config
+
+import (
+	"os"
+	"testing"
+
+	"github.com/moov-io/base/database"
+	"github.com/stretchr/testify/require"
+)
+
+func TestConfig(t *testing.T) {
+	t.Run("When DATABASE_TYPE is not set", func(t *testing.T) {
+		setenv(t, "DATABASE_TYPE", "")
+
+		conf := New()
+		err := conf.Load()
+
+		require.NoError(t, err)
+
+		require.Equal(t, conf.Database.SQLite.Path, "customers.db")
+	})
+
+	t.Run("When DATABASE_TYPE is set to mysql", func(t *testing.T) {
+		setenv(t, "DATABASE_TYPE", "mysql")
+		setenv(t, "MYSQL_USER", "user")
+		setenv(t, "MYSQL_PASSWORD", "password")
+		setenv(t, "MYSQL_ADDRESS", "tcp(localhost:1234)")
+		setenv(t, "MYSQL_DATABASE", "test")
+
+		conf := New()
+		err := conf.Load()
+
+		require.NoError(t, err)
+		require.Equal(t, "test", conf.Database.DatabaseName)
+		want := &database.MySQLConfig{
+			Address:  "tcp(localhost:1234)",
+			User:     "user",
+			Password: "password",
+		}
+		require.Equal(t, want, conf.Database.MySQL)
+	})
+}
+
+// setenv restores env variables after test
+func setenv(t *testing.T, key, val string) {
+	if oldVal, found := os.LookupEnv(key); found {
+		t.Cleanup(func() {
+			os.Setenv(key, oldVal)
+		})
+	} else {
+		t.Cleanup(func() {
+			os.Unsetenv(key)
+		})
+	}
+
+	os.Setenv(key, val)
+}
diff --git a/pkg/configuration/repository_test.go b/pkg/configuration/repository_test.go
index 49b2615..a7fc981 100644
--- a/pkg/configuration/repository_test.go
+++ b/pkg/configuration/repository_test.go
@@ -9,7 +9,7 @@ import (
 	"testing"
 
 	"github.com/moov-io/base"
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 	"github.com/moov-io/customers/pkg/client"
 )
 
diff --git a/pkg/customers/customer_search_test.go b/pkg/customers/customer_search_test.go
index e09c440..3b6c622 100644
--- a/pkg/customers/customer_search_test.go
+++ b/pkg/customers/customer_search_test.go
@@ -17,7 +17,7 @@ import (
 	"github.com/moov-io/base/log"
 	"github.com/stretchr/testify/require"
 
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 	"github.com/moov-io/customers/pkg/client"
 )
 
diff --git a/pkg/customers/customer_ssn_test.go b/pkg/customers/customer_ssn_test.go
index dbe5cc7..fefe3f9 100644
--- a/pkg/customers/customer_ssn_test.go
+++ b/pkg/customers/customer_ssn_test.go
@@ -8,7 +8,7 @@ import (
 	"encoding/base64"
 	"testing"
 
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 	"github.com/moov-io/customers/pkg/secrets"
 
 	"github.com/moov-io/base"
diff --git a/pkg/customers/customers_test.go b/pkg/customers/customers_test.go
index 552cf51..847671c 100644
--- a/pkg/customers/customers_test.go
+++ b/pkg/customers/customers_test.go
@@ -19,7 +19,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 
 	"github.com/moov-io/base"
 
diff --git a/pkg/documents/disclaimers_test.go b/pkg/documents/disclaimers_test.go
index ae7ca5e..c3434bf 100644
--- a/pkg/documents/disclaimers_test.go
+++ b/pkg/documents/disclaimers_test.go
@@ -17,7 +17,7 @@ import (
 
 	"github.com/moov-io/base"
 	"github.com/moov-io/base/admin"
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 	"github.com/moov-io/customers/pkg/client"
 
 	"github.com/gorilla/mux"
diff --git a/pkg/documents/documents_test.go b/pkg/documents/documents_test.go
index 30ab5fd..8d87c93 100644
--- a/pkg/documents/documents_test.go
+++ b/pkg/documents/documents_test.go
@@ -24,7 +24,7 @@ import (
 	"github.com/stretchr/testify/require"
 	"gocloud.dev/blob"
 
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 	"github.com/moov-io/customers/pkg/client"
 	"github.com/moov-io/customers/pkg/documents/storage"
 	"github.com/moov-io/customers/pkg/secrets"
diff --git a/pkg/documents/repository_test.go b/pkg/documents/repository_test.go
index 588ebb9..af681ec 100644
--- a/pkg/documents/repository_test.go
+++ b/pkg/documents/repository_test.go
@@ -14,8 +14,8 @@ import (
 
 	"github.com/gorilla/mux"
 	"github.com/moov-io/base"
+	"github.com/moov-io/base/database"
 	"github.com/moov-io/base/log"
-	"github.com/moov-io/customers/internal/database"
 	"github.com/moov-io/customers/pkg/client"
 	"github.com/moov-io/customers/pkg/customers"
 	"github.com/moov-io/customers/pkg/secrets"
diff --git a/pkg/reports/router_test.go b/pkg/reports/router_test.go
index 19572ca..ecd8d1c 100644
--- a/pkg/reports/router_test.go
+++ b/pkg/reports/router_test.go
@@ -13,7 +13,7 @@ import (
 	"github.com/moov-io/base/log"
 	"github.com/stretchr/testify/require"
 
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 	"github.com/moov-io/customers/pkg/accounts"
 	"github.com/moov-io/customers/pkg/client"
 	"github.com/moov-io/customers/pkg/customers"
diff --git a/pkg/validator/repository_test.go b/pkg/validator/repository_test.go
index 500f7aa..7455fab 100644
--- a/pkg/validator/repository_test.go
+++ b/pkg/validator/repository_test.go
@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/moov-io/base"
-	"github.com/moov-io/customers/internal/database"
+	"github.com/moov-io/base/database"
 	"github.com/stretchr/testify/require"
 )
 
@@ -50,5 +50,9 @@ func createTestRepository(t *testing.T) Repository {
 
 	db := database.CreateTestMySQLDB(t)
 
+	t.Cleanup(func() {
+		db.Close()
+	})
+
 	return &sqlRepository{db.DB}
 }