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.