Claude-code-plugins-plus notion-multi-env-setup
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/notion-pack/skills/notion-multi-env-setup" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-notion-multi-env-setup && rm -rf "$T"
plugins/saas-packs/notion-pack/skills/notion-multi-env-setup/SKILL.mdNotion Multi-Environment Setup
Overview
Configure separate Notion integrations for development, staging, and production. Each environment uses its own integration token, targets different databases, and applies environment-appropriate log levels and timeouts. This prevents dev data leaking into prod, isolates testing, and enforces least-privilege per tier.
Prerequisites
- Notion workspace(s) per environment (one workspace can serve dev/staging via separate integrations)
v2+ installed (@notionhq/client
)npm install @notionhq/client- Python alternative:
(notion-client
)pip install notion-client - Secret management platform (AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault)
- CI/CD pipeline with per-environment variable injection
Instructions
Step 1: Create Per-Environment Integrations and Env-Aware Client
Create separate integrations at https://www.notion.so/my-integrations with scoped capabilities:
| Environment | Integration Name | Capabilities | Timeout | Log Level |
|---|---|---|---|---|
| Development | | All (read+update+insert+delete) | 60s | DEBUG |
| Staging | | Read + Update + Insert | 30s | WARN |
| Production | | Minimum required only | 30s | ERROR |
TypeScript — environment-aware client factory:
import { Client, LogLevel } from '@notionhq/client'; interface NotionEnvConfig { token: string; databaseIds: Record<string, string>; logLevel: LogLevel; timeoutMs: number; maxRetries: number; } const ENV_DEFAULTS: Record<string, Omit<NotionEnvConfig, 'token' | 'databaseIds'>> = { development: { logLevel: LogLevel.DEBUG, timeoutMs: 60_000, maxRetries: 0 }, staging: { logLevel: LogLevel.WARN, timeoutMs: 30_000, maxRetries: 2 }, production: { logLevel: LogLevel.ERROR, timeoutMs: 30_000, maxRetries: 3 }, }; function getConfig(): NotionEnvConfig { const env = process.env.NODE_ENV || 'development'; const defaults = ENV_DEFAULTS[env] ?? ENV_DEFAULTS.development; const token = process.env.NOTION_TOKEN; if (!token) { throw new Error( `NOTION_TOKEN not set for "${env}". ` + `Set it in .env.${env} or your secret manager.` ); } return { token, databaseIds: { tasks: process.env.NOTION_TASKS_DB_ID!, users: process.env.NOTION_USERS_DB_ID!, logs: process.env.NOTION_LOGS_DB_ID!, }, ...defaults, }; } export function createNotionClient(): Client { const config = getConfig(); return new Client({ auth: config.token, logLevel: config.logLevel, timeoutMs: config.timeoutMs, }); } export function getDatabaseId(name: string): string { const config = getConfig(); const id = config.databaseIds[name]; if (!id) { throw new Error( `Database ID not configured for "${name}". ` + `Set NOTION_${name.toUpperCase()}_DB_ID in your environment.` ); } return id; }
Python — environment-aware client factory:
import os from notion_client import Client ENV_CONFIGS = { "development": {"timeout_ms": 60_000, "log_level": "DEBUG"}, "staging": {"timeout_ms": 30_000, "log_level": "WARNING"}, "production": {"timeout_ms": 30_000, "log_level": "ERROR"}, } def create_notion_client() -> Client: env = os.getenv("APP_ENV", "development") token = os.getenv("NOTION_TOKEN") if not token: raise ValueError(f"NOTION_TOKEN not set for '{env}'") cfg = ENV_CONFIGS.get(env, ENV_CONFIGS["development"]) return Client(auth=token, timeout_ms=cfg["timeout_ms"]) def get_database_id(name: str) -> str: env_var = f"NOTION_{name.upper()}_DB_ID" db_id = os.getenv(env_var) if not db_id: raise ValueError(f"{env_var} not set") return db_id
Step 2: Secret Management and Environment Files
Local development —
files (git-ignored):.env
# .env.development NOTION_TOKEN=ntn_dev_xxxxxxxxxxxxxxxxxxxxx NOTION_TASKS_DB_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee NOTION_USERS_DB_ID=ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj NOTION_LOGS_DB_ID=11111111-2222-3333-4444-555555555555 # .env.staging NOTION_TOKEN=ntn_staging_xxxxxxxxxxxxxxxxx NOTION_TASKS_DB_ID=66666666-7777-8888-9999-000000000000 NOTION_USERS_DB_ID=aaaaaaaa-1111-2222-3333-444444444444 # Production tokens NEVER stored as files — use secret manager
AWS Secrets Manager:
# Store production secrets aws secretsmanager create-secret \ --name "notion/production" \ --secret-string '{ "token": "ntn_prod_xxxxxxxxxxxxxxxxx", "tasks_db": "abcdefab-cdef-abcd-efab-cdefabcdefab", "users_db": "12345678-abcd-efgh-ijkl-123456789012" }' # Retrieve in application aws secretsmanager get-secret-value --secret-id notion/production --query SecretString --output text
GCP Secret Manager:
# Store each secret individually echo -n "ntn_prod_xxxxxxxxxxxxxxxxx" | gcloud secrets create notion-token-prod --data-file=- echo -n "abcdefab-cdef-abcd-efab-cdefabcdefab" | gcloud secrets create notion-tasks-db-prod --data-file=- # Inject into Cloud Run service gcloud run deploy my-service \ --set-secrets=NOTION_TOKEN=notion-token-prod:latest,NOTION_TASKS_DB_ID=notion-tasks-db-prod:latest
HashiCorp Vault:
vault kv put secret/notion/production \ token=ntn_prod_xxxxxxxxxxxxxxxxx \ tasks_db_id=abcdefab-cdef-abcd-efab-cdefabcdefab
Step 3: Environment Guards and CI/CD
Environment guards — prevent cross-environment mistakes:
function requireEnvironment(required: 'development' | 'staging' | 'production') { const current = process.env.NODE_ENV || 'development'; if (current !== required) { throw new Error( `This operation requires "${required}" but running in "${current}". Aborting.` ); } } // Block destructive operations in production function requireNonProduction() { if (process.env.NODE_ENV === 'production') { throw new Error('Destructive operation blocked in production'); } } // Usage async function seedTestData(notion: Client, dbId: string) { requireNonProduction(); // Throws in production await notion.pages.create({ parent: { database_id: dbId }, properties: { Name: { title: [{ text: { content: 'Test Record' } }] }, Status: { select: { name: 'Test' } }, }, }); } async function runMigration(notion: Client) { requireEnvironment('production'); // Only runs in production // ... migration logic }
Startup validation — fail fast on missing config:
function validateNotionConfig() { const env = process.env.NODE_ENV || 'development'; const required = ['NOTION_TOKEN', 'NOTION_TASKS_DB_ID']; const missing = required.filter(v => !process.env[v]); if (missing.length > 0) { throw new Error( `Missing env vars for "${env}": ${missing.join(', ')}. ` + `Check .env.${env} or your secret manager.` ); } // Validate token prefix matches environment const token = process.env.NOTION_TOKEN!; if (env === 'production' && token.includes('dev')) { throw new Error('Production detected but NOTION_TOKEN contains "dev" — likely wrong token'); } console.log(`Notion configured for ${env} (token: ${token.slice(0, 8)}...)`); }
CI/CD deployment with per-environment secrets:
# .github/workflows/deploy.yml jobs: deploy-staging: if: github.ref == 'refs/heads/develop' environment: staging env: NODE_ENV: staging NOTION_TOKEN: ${{ secrets.NOTION_TOKEN_STAGING }} NOTION_TASKS_DB_ID: ${{ secrets.NOTION_TASKS_DB_ID_STAGING }} steps: - uses: actions/checkout@v4 - run: npm ci - run: npm test - run: npm run deploy:staging deploy-production: if: github.ref == 'refs/heads/main' environment: production env: NODE_ENV: production NOTION_TOKEN: ${{ secrets.NOTION_TOKEN_PROD }} NOTION_TASKS_DB_ID: ${{ secrets.NOTION_TASKS_DB_ID_PROD }} steps: - uses: actions/checkout@v4 - run: npm ci - run: npm test - run: INTEGRATION=true npm run test:integration - run: npm run deploy:production
Output
- Separate Notion integrations per environment with scoped capabilities
- Environment-aware client factory (TypeScript and Python)
- Secrets stored in platform-appropriate secret managers (never in files for production)
- Startup validation that fails fast on misconfiguration
- Guards preventing cross-environment mistakes (no prod data in dev, no test data in prod)
- CI/CD pipeline deploying with per-environment secrets
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Missing env var | Check file or secret manager config |
| Wrong database in prod | Env var misconfigured | Add startup validation to compare token prefix with env |
| Token for wrong environment | Secret manager mapping error | Validate token prefix at startup |
| Dev data written to prod DB | Missing environment guard | Add to destructive operations |
| 401 Unauthorized | Token revoked or expired | Regenerate at notion.so/my-integrations, update secret |
not found | Page not shared with integration | Share target database with the correct env integration |
Examples
Full Initialization Pattern
import { Client } from '@notionhq/client'; // Initialize with full validation async function initNotion(): Promise<{ client: Client; dbId: string }> { validateNotionConfig(); const client = createNotionClient(); const dbId = getDatabaseId('tasks'); // Verify connectivity const me = await client.users.me({}); console.log(`Connected as: ${me.name} (${me.type})`); // Verify database access const db = await client.databases.retrieve({ database_id: dbId }); console.log(`Database: ${db.title[0]?.plain_text ?? 'Untitled'}`); return { client, dbId }; }
Quick Environment Check Script
#!/bin/bash # verify-notion-env.sh — run before deployment echo "Environment: ${NODE_ENV:-development}" echo "Token prefix: ${NOTION_TOKEN:0:8}..." curl -sf https://api.notion.com/v1/users/me \ -H "Authorization: Bearer ${NOTION_TOKEN}" \ -H "Notion-Version: 2022-06-28" \ | jq '{name: .name, type: .type, bot_owner: .bot.owner.type}' \ || echo "ERROR: Cannot authenticate with Notion"
Resources
- Notion Create Integrations
- Notion Authentication
- 12-Factor App Config
- AWS Secrets Manager
- GCP Secret Manager
- HashiCorp Vault KV
Next Steps
For monitoring your Notion integration health across environments, see
notion-observability.