Secret Management
Overview
Pivot uses GCP Secret Manager as the single source of truth for all environment secrets across dev, staging, and production environments. Secrets are managed as infrastructure-as-code using Terraform in the pivot-devops repository.
Key Benefits:
- Centralized: All secrets managed in one place (
pivot-devops/secrets/) - Version Controlled: Changes tracked through git history
- Automated: Terraform applies secrets to GCP Secret Manager
- Secure: Secrets never committed to application repositories
- Auditable: GCP Secret Manager provides access logs
Architecture
Environments
| Environment | GCP Project | Secrets File | Purpose |
|---|---|---|---|
| Dev | pivot-dev-59310 | secrets/env.dev | Development and testing |
| Staging | pivot-not-production-project | secrets/env.staging | QA and pre-production testing |
| Production | pivot-inc | secrets/env.production | Live customer data |
Directory Structure
pivot-devops/
├── secrets/ # Source of truth for all secrets
│ ├── env.dev # Dev environment secrets
│ ├── env.staging # Staging environment secrets
│ └── env.production # Production environment secrets
└── infrastructure/
└── secret-manager/
├── README.md # Documentation
├── dev/ # Terraform for pivot-dev-59310
│ ├── main.tf
│ ├── variables.tf
│ ├── terraform.tfvars
│ └── backend.tf
├── staging/ # Terraform for pivot-not-production-project
│ └── ...
└── production/ # Terraform for pivot-inc
└── ...
How It Works
1. Secrets Storage (env files)
Secrets are stored in simple KEY=VALUE format:
# secrets/env.dev
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=465
SMTP_USER=apikey
SMTP_PASS=SG.xxxxx
STRIPE_API_KEY=sk_test_xxxxx
2. Terraform Pushes to GCP
The Terraform configuration reads the env file and creates secrets in GCP Secret Manager:
# infrastructure/secret-manager/dev/main.tf
locals {
env_file_content = file("${path.module}/../../../secrets/env.dev")
secrets_map = {
for line in local.env_lines :
split("=", line)[0] => join("=", slice(split("=", line), 1, length(split("=", line))))
}
}
resource "google_secret_manager_secret" "secrets" {
for_each = local.secrets_map
secret_id = each.key
# ...
}
resource "google_secret_manager_secret_version" "versions" {
for_each = local.secrets_map
secret = google_secret_manager_secret.secrets[each.key].id
secret_data = each.value
}
3. CI/CD Pulls from GCP
During deployment, generate-env.ts fetches secrets from GCP Secret Manager and writes them to a .env file:
// pivot/functions/env/generate-env.ts
const secrets = await secretManagerClient.listSecrets({ parent });
for (const secret of secrets) {
const [version] = await secretManagerClient.accessSecretVersion({
name: `${secret.name}/versions/latest`
});
envContent += `${secretName}=${payload}\n`;
}
4. Cloud Functions Read from process.env
Firebase Cloud Functions access secrets via process.env:
// Example: SMTP configuration
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT),
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
Managing Secrets
Adding a New Secret
-
Add the secret to the appropriate env file:
cd pivot-devops
echo "NEW_SECRET=value" >> secrets/env.dev -
Preview changes:
cd infrastructure/secret-manager/dev
terraform plan -
Apply changes:
terraform apply -
The next deployment will pick up the new secret automatically.
Updating an Existing Secret
-
Edit the value in the env file:
# Edit secrets/env.dev and change the value -
Apply the change:
cd infrastructure/secret-manager/dev
terraform applyThis creates a new version of the secret in GCP (old versions are preserved).
Deleting a Secret
- Remove the line from the env file
- Run
terraform apply - The secret will be destroyed in GCP
Warning: Ensure the secret is not used by any deployed code before deleting.
Service Accounts
Each environment uses a dedicated service account for Terraform operations:
| Environment | Service Account | Key Location |
|---|---|---|
| Dev | github-actions-pivot-dev@pivot-dev-59310 | github/secrets/pivot-dev-firebase-sa.json |
| Staging | github-actions@pivot-not-production-project | github/secrets/pivot-staging-firebase-sa.json |
| Production | github-actions@pivot-inc | github/secrets/pivot-inc-firebase-sa.json |
Required IAM roles for the service account:
roles/secretmanager.admin- Create, update, delete secrets
Current Secrets by Category
Core Configuration
NODE_ENV- Environment identifier (development/staging/production)URL- Base URL for the environmentRTDB_URL- Firebase Realtime Database URL
Email (SMTP)
SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASSSMTP_FROM_EMAIL,SMTP_FROM_NAMENODEMAILER_USER,NODEMAILER_PASS,NODEMAILER_FROMINVITATION_TOKEN_SECRET- For secure invitation links
POS Integrations
LIGHTSPEED_*- Lightspeed K-Series integrationCLUSTER_*- Cluster POS integrationLIBRO_*- Libro Reserve integrationCLOVER_*- Clover POS integrationSQUARE_*- Square POS integrationSKYTAB_*- SkyTab POS integrationQUICKBOOKS_*- QuickBooks integration
Payment Processing
STRIPE_API_KEY,STRIPE_WEBHOOK_SECRET
HR Integration
ADP_*- ADP Workforce integration (12 secrets)
Push Notifications
APNS_KEY_ID,APNS_TEAM_ID,APNS_BUNDLE_ID
Other Services
ROLLBAR_ACCESS_TOKEN- Error trackingWEATHER_API_KEY,GOOGLE_WEATHER_API_KEY- Weather dataMYR_API_KEY,MYR_API_URL- MYR integration
Environment Comparison
The following table shows which secrets exist in each environment. Secrets marked as "Dev Only" need to be configured for staging/production when those integrations go live.
Secrets Present in All Environments
| Secret | Dev | Staging | Production |
|---|---|---|---|
NODEMAILER_USER | apikey | apikey | apikey |
NODEMAILER_PASS | SendGrid key | SendGrid key | SendGrid key (different) |
NODEMAILER_FROM | testing@pivotapp.ca | testing@pivotapp.ca | no-reply@pivotapp.ca |
SMTP_HOST | smtp.sendgrid.net | smtp.sendgrid.net | smtp.sendgrid.net |
SMTP_PORT | 465 | 465 | 465 |
SMTP_USER | apikey | apikey | apikey |
SMTP_PASS | SendGrid key | SendGrid key | SendGrid key (different) |
SMTP_FROM_EMAIL | testing@pivotapp.ca | testing@pivotapp.ca | no-reply@pivotapp.ca |
SMTP_FROM_NAME | Pivot | Pivot | Pivot |
URL | pivot-dev-59310.web.app | pivot-not-production-project.web.app | the.pivotapp.ca |
NODE_ENV | development | staging | production |
RTDB_URL | pivot-dev-59310-default-rtdb | pivot-not-production-project | pivot-inc |
ROLLBAR_ACCESS_TOKEN | Same across all | Same across all | Same across all |
WEATHER_API_KEY | Same across all | Same across all | Same across all |
INVITATION_TOKEN_SECRET | Same across all | Same across all | Same across all |
LIBRO_* (4 secrets) | Staging API | Staging API | Production API |
LIGHTSPEED_* (4 secrets) | Trial API | Trial API | Production API |
CLUSTER_* (2 secrets) | Same across all | Same across all | Same across all |
MYR_API_KEY | Different per env | Different per env | Different per env |
STRIPE_* (2 secrets) | Test keys | Test keys | Live keys |
APNS_* (3 secrets) | jobs.pivot.dev | jobs.pivot.qa | pivot-studio-inc |
Dev-Only Secrets (Missing from Staging/Production)
These integrations are configured in dev but not yet set up for staging/production:
ADP Integration (12 secrets)
| Secret | Dev Value | Staging | Production |
|---|---|---|---|
ADP_CLIENT_ID_CONNECTOR | 7e7a8259-96c0-4aa4-b8f3-fab1d1b3e708 | Not configured | Not configured |
ADP_CLIENT_ID_SSO | 65765b6a-fc01-4966-ad9a-507279e6d5fe | Not configured | Not configured |
ADP_CLIENT_SECRET_CONNECTOR | bdc4c6aa-7bda-4960-87f2-2cca94646160 | Not configured | Not configured |
ADP_CLIENT_SECRET_SSO | ba27a3e3-162f-48cd-9bea-6809073dcc32 | Not configured | Not configured |
ADP_REDIRECT_URI | https://the.pivotapp.ca/oauth/callback/adp | Not configured | Not configured |
ADP_SUBSCRIPTION_CLIENT_ID | v8GA4I81HR | Not configured | Not configured |
ADP_SUBSCRIPTION_CLIENT_SECRET | 6iLqmNe9x2iRebPtlWay | Not configured | Not configured |
ADP_SUBSCRIPTION_PASSWORD | Duw@KkUQd@2!ciNDDDje | Not configured | Not configured |
ADP_SUBSCRIPTION_USERNAME | adpPivotClient | Not configured | Not configured |
ADP_PUBLIC_CERT | Base64-encoded certificate | Not configured | Not configured |
ADP_PRIVATE_CERT | Base64-encoded private key | Not configured | Not configured |
Clover Integration (2 secrets)
| Secret | Dev Value | Staging | Production |
|---|---|---|---|
CLOVER_OAUTH_CLIENT_ID | EERPAEN597R8C | Not configured | Not configured |
CLOVER_OAUTH_CLIENT_SECRET | 751573ff-646e-eaa2-7d59-6d4781fc2021 | Not configured | Not configured |
QuickBooks Integration (4 secrets)
| Secret | Dev Value | Staging | Production |
|---|---|---|---|
QUICKBOOKS_API_URL | https://sandbox-quickbooks.api.intuit.com/v3 | Not configured | Not configured |
QUICKBOOKS_CLIENT_ID | ABn55y9X2sXYr3P3SdgoQZlh5gMzPOpiFbAxV0Ca8WnHSxbBNe | Not configured | Not configured |
QUICKBOOKS_CLIENT_SECRET | 4TS59UhcWGtijL0rYaKNPn42IkBSXyde1s7bY21U | Not configured | Not configured |
QUICKBOOKS_REDIRECT_URI | http://localhost:3000/oauth/callback/quickbooks | Not configured | Not configured |
SkyTab Integration (2 secrets)
| Secret | Dev Value | Staging | Production |
|---|---|---|---|
SKYTAB_CLIENT_ID | c7f924281b1b443283872f613bb2c684 | Not configured | Not configured |
SKYTAB_CLIENT_SECRET | 9cbca6ca-2645-4461-9f3a-29d48cea8d6c | Not configured | Not configured |
Square Integration (4 secrets)
| Secret | Dev Value | Staging | Production |
|---|---|---|---|
SQUARE_CLIENT_ID | sq0idp-Fh2M-VCS89__vP9VPxOtBw | Not configured | Not configured |
SQUARE_CLIENT_SECRET | sq0csp-L_IsIVrND6ierDelHCqmx6M2vVhSkIQFUK8wdCWo9HI | Not configured | Not configured |
SQUARE_REDIRECT_URI | https://pivot-not-production-project--pr755-... | Not configured | Not configured |
SQUARE_VERSION | 2025-07-16 | Not configured | Not configured |
Other Dev-Only Secrets
| Secret | Dev Value | Staging | Production |
|---|---|---|---|
GOOGLE_WEATHER_API_KEY | AIzaSyBzeGyWORFzvmMt2X0aBTMQCbzRXBQvi-w | Not configured | Not configured |
MYR_API_URL | https://api.staging.myr.io/v1/pivot | Not configured | Not configured |
Action Required
When enabling these integrations in staging or production:
- Obtain the appropriate credentials (sandbox for staging, production for prod)
- Add them to
secrets/env.stagingorsecrets/env.production - Run
terraform applyin the corresponding directory - Deploy the functions to pick up the new secrets
Legacy: GitHub Actions Secrets
Note: GitHub Actions secrets are now considered legacy/dead code.
Previously, secrets were managed in GitHub Actions and set via Terraform in pivot-devops/github/secrets/. The CI/CD pipeline would write these to .env during deployment. However, generate-env.ts now overwrites these with values from GCP Secret Manager.
The GitHub secrets configuration remains as a fallback but is no longer the source of truth. Plan to deprecate once GCP Secret Manager is fully validated in production.
Troubleshooting
Secret not available in Cloud Functions
-
Verify the secret exists in GCP:
gcloud secrets list --project=pivot-dev-59310 | grep SECRET_NAME -
Check if
env-variables.enum.tsincludes the secret (if using the enum pattern) -
Redeploy the functions to pick up the new secret
Terraform permission denied
Ensure the service account has roles/secretmanager.admin:
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:SERVICE_ACCOUNT" \
--role="roles/secretmanager.admin"
Secret value contains special characters
Values with = signs are handled correctly (the Terraform config uses join to reassemble them). For values with newlines or other special characters, consider base64 encoding.