Skip to main content

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

EnvironmentGCP ProjectSecrets FilePurpose
Devpivot-dev-59310secrets/env.devDevelopment and testing
Stagingpivot-not-production-projectsecrets/env.stagingQA and pre-production testing
Productionpivot-incsecrets/env.productionLive 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

  1. Add the secret to the appropriate env file:

    cd pivot-devops
    echo "NEW_SECRET=value" >> secrets/env.dev
  2. Preview changes:

    cd infrastructure/secret-manager/dev
    terraform plan
  3. Apply changes:

    terraform apply
  4. The next deployment will pick up the new secret automatically.

Updating an Existing Secret

  1. Edit the value in the env file:

    # Edit secrets/env.dev and change the value
  2. Apply the change:

    cd infrastructure/secret-manager/dev
    terraform apply

    This creates a new version of the secret in GCP (old versions are preserved).

Deleting a Secret

  1. Remove the line from the env file
  2. Run terraform apply
  3. 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:

EnvironmentService AccountKey Location
Devgithub-actions-pivot-dev@pivot-dev-59310github/secrets/pivot-dev-firebase-sa.json
Staginggithub-actions@pivot-not-production-projectgithub/secrets/pivot-staging-firebase-sa.json
Productiongithub-actions@pivot-incgithub/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 environment
  • RTDB_URL - Firebase Realtime Database URL

Email (SMTP)

  • SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS
  • SMTP_FROM_EMAIL, SMTP_FROM_NAME
  • NODEMAILER_USER, NODEMAILER_PASS, NODEMAILER_FROM
  • INVITATION_TOKEN_SECRET - For secure invitation links

POS Integrations

  • LIGHTSPEED_* - Lightspeed K-Series integration
  • CLUSTER_* - Cluster POS integration
  • LIBRO_* - Libro Reserve integration
  • CLOVER_* - Clover POS integration
  • SQUARE_* - Square POS integration
  • SKYTAB_* - SkyTab POS integration
  • QUICKBOOKS_* - 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 tracking
  • WEATHER_API_KEY, GOOGLE_WEATHER_API_KEY - Weather data
  • MYR_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

SecretDevStagingProduction
NODEMAILER_USERapikeyapikeyapikey
NODEMAILER_PASSSendGrid keySendGrid keySendGrid key (different)
NODEMAILER_FROMtesting@pivotapp.catesting@pivotapp.cano-reply@pivotapp.ca
SMTP_HOSTsmtp.sendgrid.netsmtp.sendgrid.netsmtp.sendgrid.net
SMTP_PORT465465465
SMTP_USERapikeyapikeyapikey
SMTP_PASSSendGrid keySendGrid keySendGrid key (different)
SMTP_FROM_EMAILtesting@pivotapp.catesting@pivotapp.cano-reply@pivotapp.ca
SMTP_FROM_NAMEPivotPivotPivot
URLpivot-dev-59310.web.apppivot-not-production-project.web.appthe.pivotapp.ca
NODE_ENVdevelopmentstagingproduction
RTDB_URLpivot-dev-59310-default-rtdbpivot-not-production-projectpivot-inc
ROLLBAR_ACCESS_TOKENSame across allSame across allSame across all
WEATHER_API_KEYSame across allSame across allSame across all
INVITATION_TOKEN_SECRETSame across allSame across allSame across all
LIBRO_* (4 secrets)Staging APIStaging APIProduction API
LIGHTSPEED_* (4 secrets)Trial APITrial APIProduction API
CLUSTER_* (2 secrets)Same across allSame across allSame across all
MYR_API_KEYDifferent per envDifferent per envDifferent per env
STRIPE_* (2 secrets)Test keysTest keysLive keys
APNS_* (3 secrets)jobs.pivot.devjobs.pivot.qapivot-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)

SecretDev ValueStagingProduction
ADP_CLIENT_ID_CONNECTOR7e7a8259-96c0-4aa4-b8f3-fab1d1b3e708Not configuredNot configured
ADP_CLIENT_ID_SSO65765b6a-fc01-4966-ad9a-507279e6d5feNot configuredNot configured
ADP_CLIENT_SECRET_CONNECTORbdc4c6aa-7bda-4960-87f2-2cca94646160Not configuredNot configured
ADP_CLIENT_SECRET_SSOba27a3e3-162f-48cd-9bea-6809073dcc32Not configuredNot configured
ADP_REDIRECT_URIhttps://the.pivotapp.ca/oauth/callback/adpNot configuredNot configured
ADP_SUBSCRIPTION_CLIENT_IDv8GA4I81HRNot configuredNot configured
ADP_SUBSCRIPTION_CLIENT_SECRET6iLqmNe9x2iRebPtlWayNot configuredNot configured
ADP_SUBSCRIPTION_PASSWORDDuw@KkUQd@2!ciNDDDjeNot configuredNot configured
ADP_SUBSCRIPTION_USERNAMEadpPivotClientNot configuredNot configured
ADP_PUBLIC_CERTBase64-encoded certificateNot configuredNot configured
ADP_PRIVATE_CERTBase64-encoded private keyNot configuredNot configured

Clover Integration (2 secrets)

SecretDev ValueStagingProduction
CLOVER_OAUTH_CLIENT_IDEERPAEN597R8CNot configuredNot configured
CLOVER_OAUTH_CLIENT_SECRET751573ff-646e-eaa2-7d59-6d4781fc2021Not configuredNot configured

QuickBooks Integration (4 secrets)

SecretDev ValueStagingProduction
QUICKBOOKS_API_URLhttps://sandbox-quickbooks.api.intuit.com/v3Not configuredNot configured
QUICKBOOKS_CLIENT_IDABn55y9X2sXYr3P3SdgoQZlh5gMzPOpiFbAxV0Ca8WnHSxbBNeNot configuredNot configured
QUICKBOOKS_CLIENT_SECRET4TS59UhcWGtijL0rYaKNPn42IkBSXyde1s7bY21UNot configuredNot configured
QUICKBOOKS_REDIRECT_URIhttp://localhost:3000/oauth/callback/quickbooksNot configuredNot configured

SkyTab Integration (2 secrets)

SecretDev ValueStagingProduction
SKYTAB_CLIENT_IDc7f924281b1b443283872f613bb2c684Not configuredNot configured
SKYTAB_CLIENT_SECRET9cbca6ca-2645-4461-9f3a-29d48cea8d6cNot configuredNot configured

Square Integration (4 secrets)

SecretDev ValueStagingProduction
SQUARE_CLIENT_IDsq0idp-Fh2M-VCS89__vP9VPxOtBwNot configuredNot configured
SQUARE_CLIENT_SECRETsq0csp-L_IsIVrND6ierDelHCqmx6M2vVhSkIQFUK8wdCWo9HINot configuredNot configured
SQUARE_REDIRECT_URIhttps://pivot-not-production-project--pr755-...Not configuredNot configured
SQUARE_VERSION2025-07-16Not configuredNot configured

Other Dev-Only Secrets

SecretDev ValueStagingProduction
GOOGLE_WEATHER_API_KEYAIzaSyBzeGyWORFzvmMt2X0aBTMQCbzRXBQvi-wNot configuredNot configured
MYR_API_URLhttps://api.staging.myr.io/v1/pivotNot configuredNot configured

Action Required

When enabling these integrations in staging or production:

  1. Obtain the appropriate credentials (sandbox for staging, production for prod)
  2. Add them to secrets/env.staging or secrets/env.production
  3. Run terraform apply in the corresponding directory
  4. 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

  1. Verify the secret exists in GCP:

    gcloud secrets list --project=pivot-dev-59310 | grep SECRET_NAME
  2. Check if env-variables.enum.ts includes the secret (if using the enum pattern)

  3. 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.