Environment & .env setup
This guide covers how to safely carry your DZBuild API credentials into your application — local dev, staging, production. Whether you're building a Node.js storefront, a Laravel/Symfony back-office, a Python data pipeline, a Go microservice, or a serverless edge function, the rules are the same.
What you need to store
For a typical storefront / back-office integration:
| Variable | Example | Notes |
|---|---|---|
DZBUILD_API_KEY | dzpk_live_aB3xT9... | Platform key secret. Treat as a password. |
DZBUILD_API_BASE | https://api.dzbuild.app/v1 | Base URL. Use the edge host in prod, never dzbuild.com/api/v1 (it's only an alias). |
DZBUILD_WEBHOOK_SECRET | dzwh_sec_... | Per-webhook secret returned at registration. Used to verify X-DZ-Signature. |
For public-key flow (signups / events from a public client):
| Variable | Example | Notes |
|---|---|---|
DZBUILD_PUBLIC_KEY_ID | dzpub_live_... | Safe to ship in client code (the id is public, the secret is not) |
DZBUILD_SIGNING_SECRET | sig_... | Server-side only — used to HMAC-sign each request body |
.env files
The simplest pattern:
# .env (in your project root, gitignored)
DZBUILD_API_KEY=dzpk_live_xxxxxxxxxxxxxxxx
DZBUILD_API_BASE=https://api.dzbuild.app/v1
DZBUILD_WEBHOOK_SECRET=dzwh_sec_xxxxxxxxxxxxxxxx
# .gitignore
.env
.env.local
.env.*.local
Commit a .env.example file with placeholder values so other developers know which vars to set:
# .env.example (committed)
DZBUILD_API_KEY=dzpk_live_REPLACE_ME
DZBUILD_API_BASE=https://api.dzbuild.app/v1
DZBUILD_WEBHOOK_SECRET=dzwh_sec_REPLACE_ME
Loading .env per language
Node.js / Next.js / Express
// next.config.js — Next.js auto-loads .env, .env.local, .env.production
// For plain Node:
import 'dotenv/config';
const key = process.env.DZBUILD_API_KEY;
Python
# pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv()
key = os.environ['DZBUILD_API_KEY']
PHP / Laravel
// Laravel auto-loads .env
$key = env('DZBUILD_API_KEY');
// Plain PHP:
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$key = $_ENV['DZBUILD_API_KEY'];
Go
// go get github.com/joho/godotenv
godotenv.Load()
key := os.Getenv("DZBUILD_API_KEY")
Flutter / mobile
Don't ship the platform key in your mobile app. Instead:
- Your mobile app calls your back-end.
- Your back-end holds the API key and proxies requests to DZBuild.
If you need the mobile client to talk directly to DZBuild (e.g. for signups), use the public-key flow — only the public key id ships, never the platform secret. See Public-key endpoints.
Production environments
In production, don't use a .env file. Use the platform's native secret store:
Vercel / Netlify / Cloudflare Pages
Project → Settings → Environment Variables
DZBUILD_API_KEY = dzpk_live_...
DZBUILD_API_BASE = https://api.dzbuild.app/v1
DZBUILD_WEBHOOK_SECRET = dzwh_sec_...
Mark prod-only vars as "Production" scope. Mark staging vars as "Preview" or a separate environment.
Cloudflare Workers
wrangler secret put DZBUILD_API_KEY
# (paste the value when prompted)
Available in the worker as env.DZBUILD_API_KEY (with [vars] declared in wrangler.toml).
AWS Lambda / API Gateway
Use AWS Secrets Manager or Parameter Store:
import boto3, json
secret = json.loads(
boto3.client('secretsmanager').get_secret_value(SecretId='dzbuild/prod')['SecretString']
)
key = secret['DZBUILD_API_KEY']
Don't store the key in Lambda env vars in plaintext (they appear in CloudTrail logs). Reference the secret manager.
Docker / Docker Compose
# docker-compose.yml
services:
app:
image: yourapp:latest
env_file:
- .env.production # NOT committed
environment:
- NODE_ENV=production
For Kubernetes, use a Secret:
apiVersion: v1
kind: Secret
metadata:
name: dzbuild-creds
type: Opaque
stringData:
DZBUILD_API_KEY: dzpk_live_...
DZBUILD_WEBHOOK_SECRET: dzwh_sec_...
Then reference in the deployment:
envFrom:
- secretRef:
name: dzbuild-creds
GitHub Actions
# .github/workflows/deploy.yml
env:
DZBUILD_API_KEY: ${{ secrets.DZBUILD_API_KEY }}
Set the secret in Repo → Settings → Secrets and variables → Actions.
Development vs production keys
Always mint two separate keys:
- One named
devorstaging— used in.env.local/ dev environments - One named
production— used only in your real prod deployment
If your dev key leaks, you only burn the dev key. Production data stays intact.
# Dashboard → Developer → API Keys
+ Create platform key → name "myapp-dev", scopes: products:read, orders:read, orders:write
+ Create platform key → name "myapp-prod", scopes: products:read, orders:read, orders:write
Use the same scopes on both, just different names.
Webhook secrets
When you register a webhook, the response includes a secret:
curl -X POST 'https://api.dzbuild.app/v1/webhooks' \
-H "Authorization: Bearer $DZBUILD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://yourapp.com/webhook",
"events": ["order.created", "order.confirmed"]
}'
Response:
{
"data": {
"id": 42,
"url": "https://yourapp.com/webhook",
"secret": "dzwh_sec_aB3xT9...", ← store this in DZBUILD_WEBHOOK_SECRET
"events": ["order.created", "order.confirmed"]
}
}
This secret is shown once — save it immediately to your secret store. If you lose it, delete the webhook and register a new one.
Use the secret to verify each webhook delivery:
// Express webhook handler
app.post('/webhook', (req, res) => {
const sig = req.headers['x-dz-signature'];
const ts = req.headers['x-dz-timestamp'];
const body = req.rawBody; // ⚠️ raw, not parsed
const expected = crypto
.createHmac('sha256', process.env.DZBUILD_WEBHOOK_SECRET)
.update(`${ts}.${body}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('bad signature');
}
// ... process the event
});
See Verifying signatures for full code in 5 languages.
Public-key flow (browser / mobile)
If you need to call DZBuild directly from the browser or mobile app for narrow-scope endpoints (signup tracking, event tracking), use the public-key flow. Only the public key id ships in client code — the signing secret stays on your back-end and signs each request body.
// Browser code — safe
const PUBLIC_KEY_ID = "dzpub_live_xxxx"; // committed in client code is fine
// Get a signature from your server (which holds DZBUILD_SIGNING_SECRET)
async function trackSignup(email, externalId) {
const nonce = crypto.randomUUID();
const body = JSON.stringify({ email, external_user_id: externalId, source: 'web', nonce });
const ts = Math.floor(Date.now() / 1000);
// Your back-end signs it (NEVER do this in the browser)
const { signature } = await fetch('/sign-dz-event', {
method: 'POST',
body: JSON.stringify({ body_hash_input: body, ts, nonce })
}).then(r => r.json());
await fetch('https://api.dzbuild.app/v1/signups', {
method: 'POST',
headers: {
'Authorization': `DZ-Public ${PUBLIC_KEY_ID}`,
'X-DZ-Timestamp': ts.toString(),
'X-DZ-Nonce': nonce,
'X-DZ-Signature': signature,
'Content-Type': 'application/json'
},
body
});
}
The signing secret never leaves your back-end. The public key id can be inspected by anyone — that's by design.
Local-dev tunneling for webhooks
DZBuild webhooks need a public HTTPS URL. To test locally, use a tunnel:
# ngrok
ngrok http 3000
# Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000
# tailscale serve
tailscale serve https / http://localhost:3000
Register the tunnel URL as your webhook target. Update it whenever the tunnel restarts (ngrok free changes URL each time; pay $8/mo for a stable subdomain).
Rotation policy
Rotate keys:
- Quarterly for production keys (set a calendar reminder)
- Immediately if a key may have leaked (committed to git, screenshotted, sent in chat)
- When a team member leaves if they had access to the secret store
Rotation procedure:
- Mint a new key in the dashboard with the same scopes.
- Update your secret store / env vars to the new key.
- Deploy. Verify traffic on the new key via
GET /v1/usage. - Once you confirm 24h of clean traffic on the new key, revoke the old one:
DELETE /v1/keys/{old_key_id}or via dashboard.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Committed .env to git | Key is now in git history forever; rotate immediately | git filter-repo won't fix forks/clones; assume compromised |
| Used the platform key in browser code | Customers can read your network tab → see and steal the key | Move to back-end / serverless function; rotate the leaked key |
Hardcoded dzpk_live_... in source | Same as above | Use env vars; rotate |
| Same key for dev + prod | A dev mistake hits prod data | Mint two keys; never share |
| Lost webhook secret | Can no longer verify deliveries | Delete the webhook, register a new one |
| Logged the API key in app logs | Auditors / log shippers / anyone with log access sees it | Redact secrets in your logger config; rotate |
Quick checklist before going live
-
.envis in.gitignore(and was never accidentally committed) - Production key is named
prodand used only in prod - Dev key is named
devand only in dev/staging - Webhook secrets stored in secret manager, not in code
-
Authorizationheader is on every API call from your back-end - Browser code never sees
dzpk_live_*(onlydzpub_live_*if you use public-key flow) - Signature verification is enabled on your webhook endpoint
- You have a key rotation reminder on your calendar
- You have logging that does NOT capture full request bodies (could include API keys in client headers)