Oct 28, 20265 min read

How to Add Secrets to a Go API

API keys and webhook signing secrets in a Go service, with Encore or environment variables backed by AWS Secrets Manager.

A Go API collects secrets quickly, such as a payments API key and a webhook signing secret. They often start in a .env file read with os.Getenv. Production then needs the values stored somewhere safer, each developer needs a local copy, and a missing variable shows up as an empty string at the first request that uses it.

This guide adds two secrets to a Go service that receives payment webhooks: an API key for outgoing calls and a signing secret that verifies incoming webhooks. It is shown with Encore and with environment variables backed by AWS Secrets Manager.

Secrets options for a Go API at a glance

CapabilityEncoreEnv vars and Secrets Manager
Declarationvar secrets struct in the serviceos.Getenv calls spread across the code
Local valuesencore secret set --type local or .secrets.local.cue.env file per developer
Production storageAWS Secrets Manager or GCP Secret ManagerSecrets Manager or SSM Parameter Store
WiringLoaded at startup by EncoreTask definition entries plus IAM
Per environmentSeparate values per environment typeOne secret per environment by hand
Readable by AI coding agentsNo, values are not stored in the repo (except a .secrets.local.cue override)Yes, the local .env file sits in the project directory

Encore: secrets declared in the service

Encore combines an open source infrastructure SDK for Go and TypeScript with a platform that deploys into your own AWS or GCP account. Secrets are fields on a package-level struct named secrets, and Encore fills them in before the service handles its first request. The webhook is a raw endpoint so the signature is checked against the exact bytes that arrived:

// Service billing receives payment webhooks. package billing import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "encore.dev/rlog" ) // Encore loads these values at startup. Set them with `encore secret set`. var secrets struct { PaymentsAPIKey string // used for outgoing calls to the payments API PaymentsWebhookSecret string // verifies incoming webhook signatures } // Webhook checks the HMAC signature before trusting the payload. // //encore:api public raw method=POST path=/webhooks/payments func Webhook(w http.ResponseWriter, req *http.Request) { body, err := io.ReadAll(http.MaxBytesReader(w, req.Body, 1<<20)) if err != nil { http.Error(w, "body too large", http.StatusRequestEntityTooLarge) return } mac := hmac.New(sha256.New, []byte(secrets.PaymentsWebhookSecret)) mac.Write(body) want := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(want), []byte(req.Header.Get("X-Signature"))) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } rlog.Info("payment webhook verified", "bytes", len(body)) w.WriteHeader(http.StatusNoContent) } // authHeader builds the header for outgoing calls to the payments API. func authHeader() string { return "Bearer " + secrets.PaymentsAPIKey }

For local development, the values can live in a .secrets.local.cue file at the app root, which you keep out of git, or be set with encore secret set --type local:

PaymentsAPIKey: "sk_test_local" PaymentsWebhookSecret: "whsec_local"

Run the service and send a signed and an unsigned request:

~/billing-app
$ encore run$ BODY='{"id":"evt_1"}'$ SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac whsec_local | awk '{print $2}')$ curl -X POST localhost:4000/webhooks/payments -H "X-Signature: $SIG" -d "$BODY" -w "%{http_code}\n"204$ curl -X POST localhost:4000/webhooks/payments -H "X-Signature: bad" -d "$BODY"invalid signature$ encore secret set --type production PaymentsWebhookSecret$ encore secret set --type production PaymentsAPIKey

In deployed environments the values are stored in AWS Secrets Manager or GCP Secret Manager in your own cloud account, and Encore provisions an IAM role (a service account on GCP) for the service. Production, development and preview environments each have their own values, so a test key never reaches production. Locally, a secret without a value is left empty and encore run prints a warning, and a deploy to an environment where a declared secret is missing reports which keys are not defined.

Secret values are not stored in the repository or in a .env file. Shared values live in Encore's secrets manager and, once deployed, in your cloud's secret manager, so an AI coding agent working in the codebase sees which secrets a service declares but never sees the values in the files it works with, and encore secret list only shows which environments have a value. The exception is a .secrets.local.cue override, a plaintext file in the app root, so keep it out of anything an agent should not read.

When an AI coding agent adds an integration, the new secret is a field in the struct it is already editing, and a missing value shows up as a warning under encore run before the change reaches production.

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

Environment variables with AWS Secrets Manager

Without Encore, the service reads environment variables and fails fast at startup when one is missing:

type Config struct { PaymentsAPIKey string PaymentsWebhookSecret string } func loadConfig() (Config, error) { c := Config{ PaymentsAPIKey: os.Getenv("PAYMENTS_API_KEY"), PaymentsWebhookSecret: os.Getenv("PAYMENTS_WEBHOOK_SECRET"), } if c.PaymentsAPIKey == "" || c.PaymentsWebhookSecret == "" { return c, errors.New("missing payments secrets") } return c, nil }

Locally the values come from a .env file loaded by a library such as godotenv. In production on ECS, each secret is created in Secrets Manager and referenced from the task definition, and ECS injects it as an environment variable when the task starts:

~/billing-app
$ aws secretsmanager create-secret --name prod/payments/api-key --secret-string "$KEY"$ aws secretsmanager create-secret --name prod/payments/webhook-secret --secret-string "$WHSEC"# task definition, containerDefinitions[0].secrets:# [{"name":"PAYMENTS_API_KEY","valueFrom":"arn:aws:secretsmanager:...:prod/payments/api-key-AbCdEf"},# {"name":"PAYMENTS_WEBHOOK_SECRET","valueFrom":"arn:aws:secretsmanager:...:prod/payments/webhook-secret-GhIjKl"}]

The task execution role needs secretsmanager:GetSecretValue on both ARNs. Values are read when the task starts, so rotating a secret means starting new tasks. Staging needs its own set of secrets and its own task definition pointing at them.

A .env file sits in the project directory, where any AI coding agent or tool with access to the repository can read it, and that is a common way keys end up in prompts, logs or commits.

How to choose

Pick Encore when you want secrets declared as part of the Go service, with local values for each developer and separate production values stored in your own AWS or GCP account. The same service can use a Postgres database whose credentials Encore manages for you.

Pick environment variables with Secrets Manager when your ECS task definitions are already managed in Terraform and the team is used to that flow.

Frequently asked questions

How do I add secrets to a Go API?

With Encore, declare var secrets struct { PaymentsAPIKey string } in the service package and set the value with encore secret set --type production PaymentsAPIKey. Encore loads the value at startup locally and in each environment, stored in AWS Secrets Manager or GCP Secret Manager in your own account.

Where should a Go service read secrets from on AWS?

Store them in AWS Secrets Manager or SSM Parameter Store and inject them as environment variables through the ECS task definition, or fetch them at startup with the AWS SDK. Avoid baking them into the image or committing them in a .env file.

How do I compare webhook signatures safely in Go?

Compute the HMAC of the raw request body with crypto/hmac and compare with hmac.Equal, which runs in constant time. Read the body once with a size limit before parsing it as JSON.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders