Oct 27, 20265 min read

How to Add Secrets to a FastAPI App

Managing API keys and webhook signing secrets in FastAPI with Encore, pydantic-settings, AWS Secrets Manager or Google Secret Manager, with per-environment values and nothing in git.

FastAPI apps collect secrets quickly, such as a payment API key, a webhook signing secret and an email provider token. They usually start in a .env file, and production needs them encrypted, scoped to the right service and different per environment.

This guide adds secrets to a FastAPI billing API with Encore, pydantic-settings, AWS Secrets Manager and Google Secret Manager. The examples read a Stripe-style API key and verify an HMAC-signed webhook.

What you need to decide:

  • Where each environment's values are stored.
  • Which code can read which secret.
  • How developers get local values without sharing production keys.
  • How rotation reaches running instances.

Secrets options for FastAPI at a glance

CapabilityEncorepydantic-settingsAWS Secrets ManagerGoogle Secret Manager
Declared inPython codeA BaseSettings classAWS console or IaCGCP console or IaC
Local values.secrets.user.toml.env fileReal secret or env varsReal secret or env vars
Per environmentencore secret set --typeEach platform's env varsOne secret per environmentOne secret per environment
ScopeShared or per serviceWhole processIAM policyIAM binding
Production storeSecrets Manager or Secret Manager in your accountWhatever sets env varsAWSGCP

Encore: secrets for your existing FastAPI app

Encore

Encore combines the secrets your FastAPI code declares with storage per environment, reading local values from a git-ignored file under encore run and storing production values in AWS Secrets Manager or GCP Secret Manager in your own account. It is an infrastructure SDK added to an existing app, so routers and dependencies stay as they are.

Encore declares infrastructure in Python code, runs it locally and provisions it in your AWS or GCP account
Infrastructure declared in Python, running locally under encore run and provisioned in your own AWS or GCP account on deploy.

Secrets are declared by name. A SharedSecret can be read from any service, while a Secret is declared by the service that reads it:

from contextlib import asynccontextmanager import encore from encore import secrets from fastapi import FastAPI # Shared across services; set per environment with `encore secret set`. stripe_key = secrets.SharedSecret[str]("StripeSecretKey") # Private to this service. webhook_secret = secrets.Secret[str]("WebhookSigningSecret") @asynccontextmanager async def lifespan(app: FastAPI): await encore.start() yield app = FastAPI(lifespan=lifespan)

Routes read .value where they need it. The webhook handler computes an HMAC over the raw body and compares it in constant time with hmac.compare_digest:

import hashlib import hmac from typing import Annotated from fastapi import Header, HTTPException, Request @app.get("/billing/mode") async def billing_mode() -> dict: # Read the value where it is used, never at import time into a log line. key = stripe_key.value return {"mode": "live" if key.startswith("sk_live_") else "test"} @app.post("/webhooks/payments", status_code=204) async def payment_webhook( request: Request, x_signature: Annotated[str, Header()] ) -> None: body = await request.body() expected = hmac.new(webhook_secret.value.encode(), body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, x_signature): raise HTTPException(status_code=401, detail="bad signature")

Local values live in .secrets.user.toml, which is git-ignored, with shared secrets under [shared] and service secrets under the service name:

[shared] StripeSecretKey = "sk_test_123" [services.billing] WebhookSigningSecret = "whsec_local"

Under encore run, a correctly signed webhook returns 204 and a wrong signature returns 401:

~/billing
$ encore run$ curl localhost:4000/billing/mode{"mode":"test"}$ curl -X POST localhost:4000/webhooks/payments -H 'x-signature: nope' -d '{}'{"detail":"bad signature"}

Production values are set from the CLI once per environment type:

~/billing
$ encore secret set --type production StripeSecretKey$ encore secret set --type production WebhookSigningSecret

On deploy, values are stored in Secrets Manager on AWS or Secret Manager on GCP in your own account, and Encore provisions an IAM role for the service.

An agent adding an integration declares the secret next to the client code and tests it with a local value, and production only needs the encore secret set call.

Good to know

Never log .value or return it in a response; the mode check above only reveals the prefix. Stripe's own signature scheme adds a timestamp to stop replays, so use the provider's library when one exists and keep the HMAC check for your own webhooks.

Try Encore →

pydantic-settings

pydantic-settings loads typed settings from environment variables and .env files, and SecretStr hides values in reprs:

from functools import lru_cache from pydantic import SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env") stripe_secret_key: SecretStr webhook_signing_secret: SecretStr @lru_cache def get_settings() -> Settings: return Settings()

Good to know

Routes take settings: Annotated[Settings, Depends(get_settings)] and call .get_secret_value(). pydantic-settings reads values but does not store them, so production still needs something that puts encrypted values into the environment.

AWS Secrets Manager

On AWS, the app fetches values at startup with boto3:

import json import boto3 client = boto3.client("secretsmanager") values = json.loads(client.get_secret_value(SecretId="billing/production")["SecretString"]) stripe_key = values["StripeSecretKey"]

Good to know

You create one secret per environment, grant the task role secretsmanager:GetSecretValue on it, and decide how local development gets values. ECS can also inject secrets as environment variables at task start, which avoids the SDK call.

Google Secret Manager

Secret Manager on GCP has the same shape:

from google.cloud import secretmanager client = secretmanager.SecretManagerServiceClient() name = "projects/my-project/secrets/stripe-secret-key/versions/latest" stripe_key = client.access_secret_version(name=name).payload.data.decode()

Good to know

The service account needs the Secret Manager Secret Accessor role on each secret. Cloud Run can mount secrets as environment variables or files with --set-secrets, which avoids the client library in the app.

How to choose

  • Encore if you want secrets declared in code, local values from a git-ignored file and production values stored in your own AWS or GCP account.
  • pydantic-settings for typed settings in a project where the platform already injects secrets.
  • AWS Secrets Manager or Google Secret Manager directly if you manage IAM and secret creation per environment yourself.

Python support in Encore is in beta, and you can sign up for access to try it with your FastAPI app.

Frequently asked questions

How do I manage secrets in a FastAPI app?

Keep secret values out of the repository, load them per environment and read them where they are used. With Encore, declare stripe_key = secrets.SharedSecret[str]("StripeSecretKey") and webhook_secret = secrets.Secret[str]("WebhookSigningSecret"), read them with .value, set local values in .secrets.user.toml and production values with encore secret set.

How do I verify a webhook signature in FastAPI?

Read the raw body with await request.body(), compute an HMAC-SHA256 over it with the signing secret, and compare it to the signature header with hmac.compare_digest. Parsing the JSON first can change the bytes and break the signature.

Is a .env file safe for FastAPI secrets?

A .env file kept out of git is fine for local development. In production, values should come from a secrets manager or the platform's secret store, so they are encrypted, access is logged and rotation does not need a new image.

What is the difference between a shared and a service secret?

A shared secret is available to every service in the app, such as a payment provider key used in several places. A service secret is visible only to the service that declares it, such as the signing secret for one webhook endpoint.

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