GCP Secret Manager
How Trovella provisions and accesses secrets in GCP Secret Manager -- Terraform module, naming conventions, IAM roles, and adding new secrets.
All production secrets live in GCP Secret Manager within the trovella-prod project. Terraform creates the secret shells (metadata only); values are set outside Terraform to keep them out of state files.
Terraform Module
The infra/modules/secret-manager/main.tf module is intentionally minimal:
resource "google_secret_manager_secret" "secret" {
for_each = toset(var.secrets)
project = var.project_id
secret_id = "trovella-${each.value}"
labels = var.labels
replication {
auto {}
}
}
Key design decisions:
trovella-prefix: Every secret ID starts withtrovella-to namespace them within the GCP project. The module prepends this automatically from the short name.- Auto replication: Secrets are replicated automatically across GCP regions. There is no need to pin a specific region for a single-region deployment.
- No versions managed: The module only creates the secret resource. Versions (actual values) are never in Terraform.
Naming Convention
Secret Manager IDs follow the pattern trovella-{service}-{purpose}:
| Pattern | Examples |
|---|---|
trovella-{provider}-api-key | trovella-anthropic-api-key, trovella-google-ai-api-key, trovella-typesense-api-key, trovella-resend-api-key |
trovella-{service}-{detail} | trovella-database-url, trovella-better-auth-secret, trovella-better-auth-url |
trovella-google-oauth-{part} | trovella-google-oauth-client-id, trovella-google-oauth-client-secret |
trovella-upstash-redis-{part} | trovella-upstash-redis-url, trovella-upstash-redis-token |
trovella-inngest-{part} | trovella-inngest-event-key, trovella-inngest-signing-key |
trovella-sentry-dsn | Single-value secrets use just the service and type |
All names use lowercase kebab-case. The short name (without the trovella- prefix) is what appears in the Terraform secrets list in infra/environments/prod/main.tf.
Production Secret List
The infra/environments/prod/main.tf file declares all 14 secrets:
module "secrets" {
source = "../../modules/secret-manager"
project_id = var.project_id
labels = local.labels
secrets = [
"anthropic-api-key",
"google-ai-api-key",
"database-url",
"better-auth-secret",
"better-auth-url",
"google-oauth-client-id",
"google-oauth-client-secret",
"resend-api-key",
"upstash-redis-url",
"upstash-redis-token",
"sentry-dsn",
"inngest-event-key",
"inngest-signing-key",
"typesense-api-key",
]
}
IAM Access Control
Two service accounts can read secrets. Both are granted roles/secretmanager.secretAccessor at the project level:
VM Service Account
Defined in infra/modules/compute-vm/main.tf:
resource "google_service_account" "vm" {
account_id = "trovella-vm-prod"
display_name = "Trovella VM (prod)"
}
resource "google_project_iam_member" "vm_secret_accessor" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.vm.email}"
}
The VM uses this service account's ambient credentials (via the GCE metadata server) to run gcloud secrets versions access in sync-secrets-vm.sh. No key files are involved.
CI Service Account (via WIF)
Defined in infra/modules/wif/main.tf:
resource "google_project_iam_member" "secret_accessor" {
for_each = { for k, v in var.target_projects : k => v if k != "shared" }
project = each.value
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.github_actions.email}"
}
GitHub Actions authenticates via Workload Identity Federation OIDC, then impersonates this service account. The grant excludes the trovella-shared project (which has no secrets). See CI Secret Access for details.
No human access by default
Individual developers do not have secretmanager.secretAccessor in production. To read a secret value for debugging, use gcloud with a privileged account or request temporary access.
Adding a New Secret
Follow this sequence when introducing a new external service or credential:
1. Add to Terraform
Add the short name to the secrets list in infra/environments/prod/main.tf:
secrets = [
# ... existing secrets ...
"new-service-api-key",
]
2. Apply Terraform
cd infra/environments/prod
terraform plan # Verify: 1 to add, 0 to change, 0 to destroy
terraform apply
This creates an empty secret named trovella-new-service-api-key in Secret Manager.
3. Set the value
echo -n "the-actual-secret-value" | \
gcloud secrets versions add trovella-new-service-api-key \
--project=trovella-prod \
--data-file=-
Use echo -n (no trailing newline) to avoid whitespace issues in connection strings and API keys.
4. Add to sync script
Add the mapping to the SECRETS array in infra/sync-secrets-vm.sh:
"trovella-new-service-api-key:NEW_SERVICE_API_KEY"
The format is secret-manager-id:ENV_VAR_NAME.
5. Add to .env.example files
Add the variable with a safe default (or empty) to both:
apps/web/.env.example-- for the web app.env.example(root) -- if the variable is used by database scripts
6. Use in code
Read from process.env["NEW_SERVICE_API_KEY"] through the appropriate @repo/* wrapper package. Follow the package boundary rules -- external SDKs must be accessed through their dedicated wrapper.
7. Deploy
Push to main. The deploy pipeline will:
- Run
sync-secrets-vm.sh(picks up the new mapping) - Write the value to
/opt/trovella/.env - Restart containers (which read the new env var)
Terraform Outputs
The secret module outputs a map of short names to full resource IDs:
output "secret_ids" {
description = "Map of short name to full secret ID"
value = { for k, v in google_secret_manager_secret.secret : k => v.id }
}
This is surfaced in the production environment outputs as secret_ids, useful for scripting and cross-referencing.
Secrets & Configuration Overview
How Trovella manages secrets via GCP Secret Manager and environment variables across local development, CI, and production.
Environment Variables
Complete reference for every environment variable in Trovella -- local defaults, CI values, production sources, and which package reads each one.