diff --git a/_magefile/ask.go b/_magefile/ask.go
new file mode 100644
index 0000000000000000000000000000000000000000..515bc3f2573fccefe23e8d414748c16124c163d7
--- /dev/null
+++ b/_magefile/ask.go
@@ -0,0 +1,34 @@
+package _magefile
+
+import (
+	"fmt"
+
+	"github.com/AlecAivazis/survey/v2"
+	"github.com/thoas/go-funk"
+)
+
+func Select(message string, options []string) int {
+	var question = []*survey.Question{
+		{
+			Name: "name",
+			Prompt: &survey.Select{
+				Message: message,
+				Options: options,
+			},
+			Validate: survey.Required,
+		},
+	}
+
+	answer := struct {
+		Name string
+	}{}
+
+	err := survey.Ask(question, &answer, survey.WithPageSize(20))
+	if err != nil {
+		fmt.Println(err.Error())
+		return 0
+	}
+
+	index := funk.IndexOf(options, answer.Name)
+	return index
+}
diff --git a/_magefile/build.go b/_magefile/build.go
new file mode 100644
index 0000000000000000000000000000000000000000..0eb335d53336892b38eed02dc05235121de74827
--- /dev/null
+++ b/_magefile/build.go
@@ -0,0 +1,134 @@
+package _magefile
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path"
+	"runtime"
+	"strings"
+
+	"github.com/magefile/mage/sh"
+)
+
+func Clean(path string) error {
+
+	cmd := exec.Command("rm", "-rf", "build")
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Dir = path
+	err := cmd.Run()
+	if err != nil {
+		return err
+	}
+
+	cmd = exec.Command("mkdir", "-p", "build")
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Dir = path
+	err = cmd.Run()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func Build(dir string, isDebug bool) error {
+	currentDir, _ := os.Getwd()
+	fullPath := currentDir + "/core/" + dir
+
+	if err := Clean(fullPath); err != nil {
+		return err
+	}
+
+	handler := path.Base(fullPath)
+	fmt.Println(fmt.Sprintf("Building %v", handler))
+
+	outputDir := fmt.Sprintf(currentDir+`/core/build/handlers/%v`, handler)
+	outputFile := fmt.Sprintf(`%v/%v`, outputDir, handler)
+	appPath := fmt.Sprintf(`gitlab.com/ship-logic/backends/backend/core/%v`, handler)
+	err := BuildGolangApp(outputFile, appPath, isDebug)
+	if err != nil {
+		return err
+	}
+
+	// Copy files in handler folder
+	err = copyFilesToBuildDir(fullPath, outputDir)
+	if err != nil {
+		return err
+	}
+
+	// Copy shared files
+	err = copyFilesToBuildDir(currentDir+"/core/shared", outputDir)
+	if err != nil {
+		return err
+	}
+
+	if isDebug {
+		// copy env file
+		err = copyFilesToBuildDir(currentDir+"/.env.debug", outputDir)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func BuildGolangApp(outputDir string, appPath string, isDebug bool) error {
+	commandArgs := []string{
+		`build`,
+		fmt.Sprintf(`-ldflags=-X gitlab.com/ship-logic/backends/backend/globals.BuildVersion=%v -X gitlab.com/ship-logic/backends/backend/globals.OSType=%v -X gitlab.com/ship-logic/backends/backend/globals.IsDebugBuild=%v`, CurrentCommit(), runtime.GOOS, isDebug),
+	}
+
+	if isDebug {
+		commandArgs = append(commandArgs, "-gcflags", "all=-N -l")
+	}
+
+	commandArgs = append(commandArgs, `-o`,
+		outputDir,
+		appPath)
+
+	env := map[string]string{
+		"GOOS": "linux",
+	}
+
+	if !isDebug {
+		if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { // M1 mac
+			// Change to intel architecture (used when deploying locally)
+			env["GOARCH"] = "amd64"
+		}
+	}
+
+	err := sh.RunWith(env, "go", commandArgs...)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func BuildMJML(filename string) error {
+	// Get the full path to the file
+	currentDir, _ := os.Getwd()
+	fullFilePath := currentDir + "/" + filename
+	fmt.Println(fullFilePath)
+
+	// Trim the extension from the filename
+	if strings.HasSuffix(fullFilePath, ".mjml") {
+		fullFilePath = strings.TrimSuffix(fullFilePath, ".mjml")
+	}
+
+	commandArgs := []string{
+		"-l strict",
+		"--config.beautify=true",
+		fmt.Sprintf("-r %v.mjml", fullFilePath),
+		fmt.Sprintf("-o%v.html", fullFilePath),
+	}
+
+	err := sh.RunWith(nil, "mjml", commandArgs...)
+	if err != nil {
+		return err
+	}
+	return nil
+
+}
diff --git a/_magefile/cdk.go b/_magefile/cdk.go
new file mode 100644
index 0000000000000000000000000000000000000000..3ce59b8c974393923bab1adbc4c33527a6f98f94
--- /dev/null
+++ b/_magefile/cdk.go
@@ -0,0 +1,260 @@
+package _magefile
+
+import (
+	_ "embed"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+
+	"gopkg.in/yaml.v2"
+)
+
+func CDKCompileTypeScript(cdkDir string) error {
+	fmt.Println("Compiling Typescript")
+
+	cmd := exec.Command("tsc")
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Dir = cdkDir
+	err := cmd.Run()
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func CDKDiff(cdkDir string, env string, stack string, local bool) error {
+	fmt.Println("Getting Diff for: " + stack)
+
+	commandArgs := []string{
+		`diff`,
+		stack,
+		`-c`,
+		fmt.Sprintf(`config=%v`, env),
+		`-c`,
+		fmt.Sprintf(`exclusively=%v`, stack),
+		`-c`,
+		`aws-cdk:enableDiffNoFail=true`,
+	}
+
+	if local {
+		commandArgs = append(commandArgs, fmt.Sprintf(`--profile=shiplogic-%v`, env))
+	}
+
+	commandArgs = append(commandArgs,
+		`--require-approval=never`,
+		`--exclusively=true`,
+		`--progress=events`)
+
+	fmt.Println(fmt.Sprintf("Running command: cdk %s", strings.Join(commandArgs, " ")))
+	cmd := exec.Command("cdk", commandArgs...)
+	cmd.Dir = cdkDir
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Println(string(output))
+		return err
+	}
+	fmt.Println(string(output))
+	return nil
+}
+
+func CDKDeploy(cdkDir string, env string, stack string, exclusively bool, local bool) error {
+	commandArgs := []string{
+		`deploy`,
+		fmt.Sprintf(`"%s"`, stack),
+		`-c`,
+		fmt.Sprintf(`config=%v`, env),
+	}
+
+	if exclusively {
+		commandArgs = append(commandArgs, `-c`,
+			fmt.Sprintf(`exclusively="%v"`, stack))
+	}
+
+	if local {
+		commandArgs = append(commandArgs, fmt.Sprintf(`--profile=shiplogic-%v`, env))
+		commandArgs = append(commandArgs, `--hotswap`) // make it go fast
+	}
+
+	commandArgs = append(commandArgs,
+		`--require-approval=never`,
+		fmt.Sprintf(`--exclusively=%v`, strconv.FormatBool(exclusively)),
+		`--progress=events`,
+		`--app=./cdk.out`)
+
+	fmt.Println(fmt.Sprintf("Running command: cdk %s", strings.Join(commandArgs, " ")))
+
+	cmd := exec.Command("cdk", commandArgs...)
+	cmd.Dir = cdkDir
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Println(string(output))
+		return err
+	}
+	fmt.Printf("==================%v==================\n", stack)
+	fmt.Println(string(output))
+	fmt.Printf("==================%v==================\n\n\n\n", stack)
+	return nil
+}
+
+func CDKSynthAll(cdkDir string, env string, local bool) error {
+	commandArgs := []string{
+		`synth`,
+		`"*"`,
+		`-c`,
+		fmt.Sprintf(`config=%v`, env),
+		`-c`,
+		`aws-cdk:enableDiffNoFail=true`,
+	}
+
+	if local {
+		commandArgs = append(commandArgs, fmt.Sprintf(`--profile=shiplogic-%v`, env))
+	}
+
+	commandArgs = append(commandArgs,
+		`--progress=events`)
+
+	fmt.Println(fmt.Sprintf("Running command: cdk %s", strings.Join(commandArgs, " ")))
+	cmd := exec.Command("cdk", commandArgs...)
+	cmd.Dir = cdkDir
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Println("Failed to run CDK Synth")
+		fmt.Println(err)
+		fmt.Println(string(output))
+		return err
+	}
+	fmt.Println(string(output))
+	return nil
+}
+
+func CDKSynthDebug(cdkDir string, env string, handler string) error {
+	// Check if sandbox environment is active and get the sandbox name from the config file
+	sandboxFilePath := "cdk/sandbox/config.dev.sandbox.yaml"
+	sandboxName := ""
+	if _, err := os.Stat(sandboxFilePath); err == nil {
+		sandboxYamlFile, err := ioutil.ReadFile(sandboxFilePath)
+		if err != nil {
+			return err
+		}
+
+		var sandboxData = make(map[string]interface{})
+		err = yaml.Unmarshal(sandboxYamlFile, &sandboxData)
+		if err != nil {
+			return err
+		}
+
+		overrides := sandboxData["Overrides"].(map[interface{}]interface{})
+		sandboxName = overrides["SandboxName"].(string)
+	}
+
+	var stack string
+	if len(sandboxName) == 0 {
+		stack = fmt.Sprintf("shiplogic-backend-%v-core", env)
+	} else {
+		stack = fmt.Sprintf("sl-%v-%v-core", env, sandboxName)
+	}
+
+	commandArgs := []string{
+		`synth`,
+		stack,
+		`-c`,
+		fmt.Sprintf(`config=%v`, env),
+		fmt.Sprintf("--profile=shiplogic-%v", env),
+		`--require-approval=never`,
+		`--exclusively=true`,
+		`--no-staging`,
+	}
+
+	fmt.Println(fmt.Sprintf("Running command: cdk %s", strings.Join(commandArgs, " ")))
+
+	cmd := exec.Command("cdk", commandArgs...)
+	cmd.Dir = cdkDir
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Println(string(output))
+		return err
+	}
+
+	file, err := os.Create("core/" + handler + "/template.yml")
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+
+	_, err = file.WriteString(string(output))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+var (
+	//go:embed clear-cdk-cache.sh
+	script []byte
+)
+
+func ClearCDKCache(cdkDir string) error {
+	cmd := exec.Command("rm", "-rf", "./cdk.out")
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Dir = cdkDir
+	err := cmd.Run()
+	if err != nil {
+		return err
+	}
+	return nil
+
+	// runScript := fmt.Sprintf("%s", string(script))
+	//
+	// var out bytes.Buffer
+	// cmd := exec.Command("/bin/bash", "-s")
+	// cmd.Stdin = bytes.NewBuffer([]byte(runScript))
+	// cmd.Dir = cdkDir
+	// cmd.Stderr = os.Stderr
+	// cmd.Stdout = os.Stdout
+	// err := cmd.Run()
+	// fmt.Println(string(out.Bytes()))
+	// if err != nil {
+	// 	return err
+	// }
+	// return nil
+}
+
+func FunctionFromTemplate(path string) ([]string, error) {
+	yamlFile, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	template := map[string]interface{}{}
+	err = yaml.Unmarshal(yamlFile, template)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := template["Resources"].(map[interface{}]interface{})
+
+	functions := []string{}
+	for _, function := range resources {
+		function := function.(map[interface{}]interface{})
+		if function["Type"] == "AWS::Lambda::Function" {
+			properties := function["Properties"].(map[interface{}]interface{})
+			name, exists := properties["FunctionName"]
+			if exists {
+				functions = append(functions, name.(string))
+			}
+
+		}
+	}
+
+	return functions, nil
+}
diff --git a/_magefile/clear-cdk-cache.sh b/_magefile/clear-cdk-cache.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5b675c616699e48339feb09765f5a7882abc56c9
--- /dev/null
+++ b/_magefile/clear-cdk-cache.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+set -e
+
+# Check what will be deleted:
+#(cd ../cdk; find . -wholename "*/cdk.out/asset.*" -type d -prune -print | xargs du -chs)
+#(cd ../cdk; find . -wholename "*/cdk.out/.cache" -type d -prune -print | xargs du -chs)
+
+# Actually delete them:
+(cd ../cdk; find . -wholename "*/cdk.out/asset.*" -type d -prune -print -exec rm -rf '{}' \;)
+(cd ../cdk; find . -wholename "*/cdk.out/.cache" -type d -prune -print -exec rm -rf '{}' \;)
\ No newline at end of file
diff --git a/_magefile/database.go b/_magefile/database.go
new file mode 100644
index 0000000000000000000000000000000000000000..195faf252115f5625fb926ce73455552022bfd5e
--- /dev/null
+++ b/_magefile/database.go
@@ -0,0 +1 @@
+package _magefile
diff --git a/_magefile/debug.go b/_magefile/debug.go
new file mode 100644
index 0000000000000000000000000000000000000000..7ba21068a63353c8175e7e48632e009888e900d6
--- /dev/null
+++ b/_magefile/debug.go
@@ -0,0 +1,135 @@
+package _magefile
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"os/signal"
+	"os/user"
+	"syscall"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client"
+)
+
+func SamStartApi(ctx context.Context, env string, handler string, debug bool) error {
+	stopRunningSamDocker()
+
+	usr, _ := user.Current()
+	homeDir := usr.HomeDir
+	delveDir := homeDir + "/go/delve/"
+
+	commandArgs := []string{
+		`local`,
+		`start-api`,
+	}
+
+	if debug {
+		commandArgs = append(commandArgs, `--debug-port=5986`,
+			fmt.Sprintf(`--debugger-path=%s`, delveDir),
+			`--debug-args=-delveAPI=2`)
+	} else {
+		commandArgs = append(commandArgs, `--warm-containers`, `EAGER`)
+	}
+
+	commandArgs = append(commandArgs,
+		fmt.Sprintf(`--profile=shiplogic-%v`, env),
+		fmt.Sprintf(`--template=core/%v/template.yml`, handler),
+		`--region=af-south-1`)
+
+	err := runLongRunningSubProcess(ctx, "sam", commandArgs...)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func SamInvokeSQS(ctx context.Context, env string, handler string, function string) error {
+	stopRunningSamDocker()
+
+	usr, _ := user.Current()
+	homeDir := usr.HomeDir
+	delveDir := homeDir + "/go/delve/"
+
+	commandArgs := []string{
+		`local`,
+		`invoke`,
+		`-e`,
+		fmt.Sprintf(`core/%v/sqs.input.json`, handler),
+		function,
+		`--debug-port=5986`,
+		fmt.Sprintf(`--debugger-path=%s`, delveDir),
+		`--debug-args=-delveAPI=2`,
+		fmt.Sprintf(`--profile=shiplogic-%v`, env),
+		fmt.Sprintf(`--template=core/%v/template.yml`, handler),
+		`--region=af-south-1`,
+	}
+
+	err := runLongRunningSubProcess(ctx, "sam", commandArgs...)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func runLongRunningSubProcess(ctx context.Context, name string, arg ...string) error {
+	cancelContext, cancel := context.WithCancel(ctx)
+	signalChan := make(chan os.Signal, 1)
+	signal.Notify(signalChan, os.Interrupt)
+	defer func() {
+		signal.Stop(signalChan)
+		cancel()
+	}()
+
+	cmd := exec.CommandContext(cancelContext, name, arg...)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+
+	go func() {
+		select {
+		case <-signalChan: // first signal, cancel context
+			fmt.Println("Stopping process")
+			if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
+				log.Fatal("failed to kill process: ", err)
+			}
+			cancel()
+		case <-cancelContext.Done():
+		}
+		<-signalChan // second signal, hard exit
+		os.Exit(1)
+	}()
+
+	err := cmd.Run()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func stopRunningSamDocker() {
+	cli, err := client.NewEnvClient()
+	if err != nil {
+		panic(err)
+	}
+
+	containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
+	if err != nil {
+		panic(err)
+	}
+
+	for _, container := range containers {
+		for _, port := range container.Ports {
+			if port.PrivatePort == 5986 {
+				err = cli.ContainerStop(context.Background(), container.ID, nil)
+				if err != nil {
+					panic(err)
+				}
+			}
+		}
+	}
+
+}
diff --git a/_magefile/file_system.go b/_magefile/file_system.go
new file mode 100644
index 0000000000000000000000000000000000000000..0bce74e6f9373e2bac9f8ebe6c01d356a9f02648
--- /dev/null
+++ b/_magefile/file_system.go
@@ -0,0 +1,170 @@
+package _magefile
+
+import (
+	"context"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/signal"
+	"path"
+	"path/filepath"
+
+	"github.com/thoas/go-funk"
+
+	"github.com/magefile/mage/sh"
+)
+
+func GetDirs(path string) []os.FileInfo {
+	filesDir, err := ioutil.ReadDir(path)
+	// filesDir, err := ioutil.ReadDir("/Users/johan/src/Shiplogic/backends/backend/billing")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	dirs := funk.Filter(filesDir, func(info os.FileInfo) bool {
+		if info.IsDir() && info.Name() == "build" {
+			return false
+		}
+
+		return info.IsDir()
+	}).([]os.FileInfo)
+
+	return dirs
+}
+
+func copyFilesToBuildDir(currentDir string, buildDir string) error {
+	files, err := AllFiles(currentDir)
+	if err != nil {
+		return err
+	}
+	for _, file := range files {
+		fileExtension := filepath.Ext(file)
+		if fileExtension == ".go" || fileExtension == ".md" || fileExtension == ".html" || fileExtension == ".mjml" || fileExtension == ".txt" {
+			continue
+		}
+
+		// Copy files to build DIR
+		err := Copy(file, buildDir+"/"+path.Base(file))
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func AllFiles(dir string) ([]string, error) {
+	files := []string{}
+	err := filepath.Walk(dir,
+		func(path string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			if info.IsDir() {
+				return nil
+			}
+
+			files = append(files, path)
+			return nil
+		})
+
+	if err != nil {
+		return files, err
+	}
+	return files, nil
+}
+
+func Copy(src, dst string) error {
+	fileInfo, err := os.Stat(src)
+	if err != nil {
+		return err
+	}
+
+	if fileInfo.Mode() == os.ModeSymlink {
+		src, err = os.Readlink(src)
+		if err != nil {
+			return err
+		}
+	}
+
+	in, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer in.Close()
+
+	out, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	_, err = io.Copy(out, in)
+	if err != nil {
+		return err
+	}
+
+	err = os.Chmod(dst, fileInfo.Mode())
+	if err != nil {
+		return err
+	}
+
+	return out.Close()
+}
+
+func CurrentBasePath() string {
+	currentDir, _ := os.Getwd()
+	base := path.Base(currentDir)
+	return base
+}
+
+func CurrentEnv() string {
+
+	// Get env from CI commit branch
+	env := os.Getenv("CI_COMMIT_BRANCH")
+
+	// Get env from current git branch
+	if env == "" {
+		commandArgs := []string{
+			`rev-parse`,
+			`--abbrev-ref`,
+			`HEAD`,
+		}
+
+		var err error
+		env, err = sh.Output("git", commandArgs...)
+		if err != nil {
+			return ""
+		}
+	}
+
+	if env != "dev" && env != "stage" && env != "prod" {
+		env = "dev"
+	}
+	return env
+}
+
+const (
+	exitCodeErr       = 1
+	exitCodeInterrupt = 2
+)
+
+func killWithContext(ctx context.Context) {
+	cancelContext, cancel := context.WithCancel(ctx)
+	signalChan := make(chan os.Signal, 1)
+	signal.Notify(signalChan, os.Interrupt)
+	defer func() {
+		signal.Stop(signalChan)
+		cancel()
+	}()
+	go func() {
+		select {
+		case <-signalChan: // first signal, cancel context
+			cancel()
+			os.Exit(exitCodeInterrupt)
+		case <-cancelContext.Done():
+		}
+		<-signalChan // second signal, hard exit
+		os.Exit(exitCodeInterrupt)
+	}()
+}
diff --git a/_magefile/git.go b/_magefile/git.go
new file mode 100644
index 0000000000000000000000000000000000000000..57dc782cb1a9f6a050306341faef62ef139758a1
--- /dev/null
+++ b/_magefile/git.go
@@ -0,0 +1,45 @@
+package _magefile
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+)
+
+func HasChanges(previousCommit string, folder string) bool {
+	fmt.Println(fmt.Sprintf("Comparing changes from: %v for %v", previousCommit, folder))
+	commandArgs := []string{
+		`diff`,
+		`--name-only`,
+		previousCommit + `..HEAD`,
+		folder,
+	}
+
+	cmd := exec.Command("git", commandArgs...)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Println(err)
+	}
+	changes := strings.Fields(string(output))
+	if len(changes) > 0 {
+		fmt.Println(fmt.Sprintf("Has %v changes", folder))
+		return true
+	}
+	fmt.Println("No changes")
+	return false
+}
+
+func CurrentCommit() string {
+
+	commandArgs := []string{
+		`rev-parse`,
+		`--short`,
+		`HEAD`,
+	}
+	cmd := exec.Command("git", commandArgs...)
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		fmt.Println(err)
+	}
+	return string(output)
+}
diff --git a/_magefile/s3.go b/_magefile/s3.go
new file mode 100644
index 0000000000000000000000000000000000000000..e7e4aaf479058d308e536f11e276e35f93d8d9e3
--- /dev/null
+++ b/_magefile/s3.go
@@ -0,0 +1,63 @@
+package _magefile
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"os"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/s3"
+)
+
+func UploadToS3(env string, bucket string, filePath string, name string, local bool) error {
+	fmt.Println(fmt.Sprintf("Uploading %s to %s", filePath, bucket))
+
+	options := session.Options{
+		Config: aws.Config{
+			Region: aws.String("af-south-1"),
+		},
+	}
+
+	if local {
+		options.Profile = fmt.Sprintf("shiplogic-%s", env)
+	}
+
+	sess, err := session.NewSessionWithOptions(options)
+	if err != nil {
+		return err
+	}
+
+	// Open the file for use
+	file, err := os.Open(filePath)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+
+	// Get file size and read the file content into a buffer
+	fileInfo, _ := file.Stat()
+	var size = fileInfo.Size()
+	buffer := make([]byte, size)
+	_, err = file.Read(buffer)
+	if err != nil {
+		return err
+	}
+
+	// Upload to s3
+	putInput := &s3.PutObjectInput{
+		Bucket:        aws.String(bucket),
+		Key:           aws.String(name),
+		Body:          bytes.NewReader(buffer),
+		ContentLength: aws.Int64(size),
+		ContentType:   aws.String(http.DetectContentType(buffer)),
+	}
+	svc := s3.New(sess)
+	_, err = svc.PutObject(putInput)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/mage_ci.go b/mage_ci.go
new file mode 100644
index 0000000000000000000000000000000000000000..f047595f3bc67f2bb06a580ebf5b4d8ca205da7c
--- /dev/null
+++ b/mage_ci.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+	"os"
+
+	"github.com/magefile/mage/mage"
+)
+
+func main() { os.Exit(mage.Main()) }
+
+// func main() {
+// 	DebugSQS(nil)
+// }
diff --git a/mage_debug.go b/mage_debug.go
new file mode 100644
index 0000000000000000000000000000000000000000..efe4cb60f5591ddd950a2851574d6a03550a2ee9
--- /dev/null
+++ b/mage_debug.go
@@ -0,0 +1,112 @@
+// +build mage
+
+package main
+
+import (
+	"context"
+	"sync"
+
+	"github.com/magefile/mage/mg"
+
+	"gitlab.com/ship-logic/backends/backend/_magefile"
+)
+
+// ====================================================================================
+// region Debug
+// ====================================================================================
+
+// build a debug build and start sam
+func Debug(ctx context.Context) error {
+	mg.Deps(clearCDKCache)
+	mg.Deps(compileCDKTypeScript)
+
+	// Build
+	api := "handler_api"
+	if err := _magefile.Build(api, true); err != nil {
+		return err
+	}
+
+	// Create template file
+	env := _magefile.CurrentEnv()
+	cdkDir := "./cdk"
+	if err := _magefile.CDKSynthDebug(cdkDir, env, api); err != nil {
+		return err
+	}
+
+	err := _magefile.SamStartApi(ctx, env, api, true)
+	return err
+}
+
+// build a debug build and invoke the sqs queue
+func DebugSQS(ctx context.Context) error {
+	mg.Deps(clearCDKCache)
+	mg.Deps(compileCDKTypeScript)
+
+	// Build
+	sqs := "handler_sqs"
+	if err := _magefile.Build(sqs, true); err != nil {
+		return err
+	}
+
+	// Create template file
+	cdkDir := "./cdk"
+	env := _magefile.CurrentEnv()
+	if err := _magefile.CDKSynthDebug(cdkDir, env, sqs); err != nil {
+		return err
+	}
+
+	functions, err := _magefile.FunctionFromTemplate("core/" + sqs + "/template.yml")
+	if err != nil {
+		return err
+	}
+
+	index := _magefile.Select("Choose a function:", functions)
+	functionName := functions[index]
+
+	err = _magefile.SamInvokeSQS(ctx, env, sqs, functionName)
+	return err
+}
+
+// used from golang to build a debug function
+func BuildDebugFunction() error {
+	var waitGroup sync.WaitGroup
+	for _, handler := range handlers {
+		waitGroup.Add(1)
+		go func(handler string) {
+			if err := _magefile.Build(handler, true); err != nil {
+				panic(err)
+			}
+			waitGroup.Done()
+		}(handler)
+	}
+	waitGroup.Wait()
+	return nil
+}
+
+// endregion
+
+// ====================================================================================
+// region Run
+// ====================================================================================
+
+func Run(ctx context.Context) error {
+	mg.Deps(clearCDKCache)
+	mg.Deps(compileCDKTypeScript)
+	// Build
+	api := "handler_api"
+	if err := _magefile.Build(api, true); err != nil {
+		return err
+	}
+
+	// Create template file
+	env := _magefile.CurrentEnv()
+	cdkDir := "./cdk"
+	if err := _magefile.CDKSynthDebug(cdkDir, env, api); err != nil {
+		return err
+	}
+
+	err := _magefile.SamStartApi(ctx, env, api, false)
+	return err
+}
+
+// endregion
diff --git a/magefile.go b/magefile.go
new file mode 100644
index 0000000000000000000000000000000000000000..f7f49cb0c941b7b92a4bc87cf6b48c792f7eb273
--- /dev/null
+++ b/magefile.go
@@ -0,0 +1,277 @@
+// +build mage
+
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	"github.com/magefile/mage/mg"
+	"gitlab.com/ship-logic/backends/backend/_magefile"
+)
+
+var handlers = []string{
+	"handler_adhoc",
+	"handler_api",
+	"handler_cron",
+	"handler_sqs",
+}
+
+// ====================================================================================
+// region Build
+// ====================================================================================
+
+// Builds all the microservices
+func Build() error {
+	var waitGroup sync.WaitGroup
+	for _, handler := range handlers {
+		waitGroup.Add(1)
+		go func(handler string) {
+			if err := _magefile.Build(handler, false); err != nil {
+				panic(err)
+			}
+			waitGroup.Done()
+		}(handler)
+	}
+	waitGroup.Wait()
+	return nil
+}
+
+func BuildWorkers() error {
+	if err := _magefile.BuildGolangApp("workers-ec2/high-priority", "./workers-ec2/high-priority", false); err != nil {
+		return err
+	}
+	if err := _magefile.BuildGolangApp("workers-ec2/medium-priority", "./workers-ec2/medium-priority", false); err != nil {
+		return err
+	}
+	if err := _magefile.BuildGolangApp("workers-ec2/low-priority", "./workers-ec2/low-priority", false); err != nil {
+		return err
+	}
+	return nil
+}
+
+// endregion
+
+// ====================================================================================
+// region CDK
+// ====================================================================================
+
+func compileCDKTypeScript() error {
+	cdkDir := "./cdk"
+	return _magefile.CDKCompileTypeScript(cdkDir)
+}
+
+func clearCDKCache() error {
+	cdkDir := "./cdk"
+	fmt.Println("Clean CDK Cache")
+	if err := _magefile.ClearCDKCache(cdkDir); err != nil {
+		return err
+	}
+	return nil
+}
+
+func SetupCDK(local bool) {
+	env := _magefile.CurrentEnv()
+	cdkDir := "./cdk"
+
+	mg.Deps(Build)
+	mg.Deps(clearCDKCache)
+	mg.Deps(compileCDKTypeScript)
+	mg.Deps(mg.F(_magefile.CDKSynthAll, cdkDir, env, local))
+}
+
+// Run a stack diff
+func Diff(local bool) error {
+	mg.Deps(mg.F(SetupCDK, local))
+
+	env := _magefile.CurrentEnv()
+	stack := fmt.Sprintf("shiplogic-backend-%v-core", env)
+
+	// CDK Diff
+	cdkDir := "./cdk"
+	return _magefile.CDKDiff(cdkDir, env, stack, local)
+}
+
+// Use CDK to deploy the base infrastructure to AWS.
+func DeployBase(local bool) error {
+	mg.Deps(mg.F(SetupCDK, local))
+
+	cdkDir := "./cdk"
+	env := _magefile.CurrentEnv()
+	baseStacks := []string{
+		"infra",
+		"infra-us-east",
+		"cognito",
+		"policies",
+	}
+
+	for _, stack := range baseStacks {
+		stackName := fmt.Sprintf("shiplogic-backend-%v-%v", env, stack)
+		fmt.Println("Deploying: " + stack)
+		if err := _magefile.CDKDeploy(cdkDir, env, stackName, false, local); err != nil {
+			return err
+		}
+	}
+
+	// Frontend
+	stack := fmt.Sprintf("shiplogic-frontend-%v", env)
+	fmt.Println("Deploying: " + stack)
+	if err := _magefile.CDKDeploy(cdkDir, env, stack, false, local); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Use CDK to deploy all the service monitoring stacks
+func DeployServiceMonitoringStack(local bool) error {
+	mg.Deps(mg.F(SetupCDK, local))
+
+	env := _magefile.CurrentEnv()
+	stack := fmt.Sprintf("shiplogic-backend-%v-service-monitoring", _magefile.CurrentEnv())
+	if err := _magefile.CDKDeploy("./cdk", env, stack, false, local); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Use CDK to deploy a specific microservice to AWS. Needs to be in the format `mage deploy billing`
+func DeployCore(local bool) error {
+	mg.Deps(mg.F(SetupCDK, local))
+
+	// CDK deploy
+	env := _magefile.CurrentEnv()
+	stack := fmt.Sprintf("shiplogic-backend-%v-core", env)
+
+	// CDK deploy
+	fmt.Println("Deploying: " + stack)
+	return _magefile.CDKDeploy("./cdk", env, stack, true, local)
+}
+
+// endregion
+
+// ====================================================================================
+// region CI
+// ====================================================================================
+
+func RunCI(previousCommit string, local bool) error {
+	mg.Deps(mg.F(SetupCDK, local))
+
+	// Deploy base
+	hasCDKChanges := _magefile.HasChanges(previousCommit, "cdk")
+	if hasCDKChanges {
+		fmt.Println("Deploying base")
+		if err := DeployBase(local); err != nil {
+			return err
+		}
+	}
+
+	// Deploy core
+	utilsChanged := _magefile.HasChanges(previousCommit, "utils")
+	coreChanged := _magefile.HasChanges(previousCommit, "core")
+	if coreChanged || utilsChanged || hasCDKChanges {
+		err := DeployCore(local)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func DeployWorkers(previousCommit string, local bool) error {
+	env := _magefile.CurrentEnv()
+
+	hasWorkerChanges := _magefile.HasChanges(previousCommit, "workers-ec2")
+	if !hasWorkerChanges && env == "dev" {
+		return nil
+	}
+
+	mg.Deps(mg.F(BuildWorkers))
+
+	bucket := fmt.Sprintf("shiplogic-backend-%s-infra-worker-update", env)
+
+	if err := _magefile.UploadToS3(env, bucket, "./workers-ec2/high-priority/high-priority", "high-priority", local); err != nil {
+		return err
+	}
+	if err := _magefile.UploadToS3(env, bucket, "./workers-ec2/medium-priority/medium-priority", "medium-priority", local); err != nil {
+		return err
+	}
+	if err := _magefile.UploadToS3(env, bucket, "./workers-ec2/low-priority/low-priority", "low-priority", local); err != nil {
+		return err
+	}
+
+	// Create version file
+	file, err := ioutil.TempFile(os.TempDir(), "version")
+	if err != nil {
+		return err
+	}
+	defer os.Remove(file.Name())
+
+	// Save current commit in version file
+	commitHash := _magefile.CurrentCommit()
+	_, err = file.Write([]byte(strings.TrimSuffix(commitHash, "\n")))
+	if err != nil {
+		return err
+	}
+
+	// Upload to s3
+	if err := _magefile.UploadToS3(env, bucket, file.Name(), "version", local); err != nil {
+		return err
+	}
+	return nil
+}
+
+// endregion
+
+// ====================================================================================
+// region MJML
+// ====================================================================================
+
+// Builds all the microservices
+func BuildMJML() error {
+	// Install mjml with `npm install -g mjml`
+
+	// Recursively traverse all folders in the microservice to find and build MJML files.
+	err := filepath.Walk("core",
+		func(path string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			if info.Mode().IsDir() {
+				// Ignore "build" directories
+				if info.Name() == "build" {
+					return nil
+				}
+
+				// Get all the subdirectories
+				files, err := ioutil.ReadDir(path)
+				if err != nil {
+					return err
+				}
+
+				// Build all MJML files
+				for _, f := range files {
+					if strings.HasSuffix(f.Name(), ".mjml") {
+						mjmlFilePath := path + "/" + f.Name()
+						err = _magefile.BuildMJML(mjmlFilePath)
+						if err != nil {
+							return err
+						}
+					}
+				}
+			}
+			return nil
+		})
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// endregion