Files
Will Anderson cd2c22c295 add marketing-dev environment - team-internal auto-deploy zone
dev.neurontechnologies.ai mirrors stage in shape but looser:
broken builds OK, auto-deploys on every push to dev branch (once
CI lands), dev Supabase project (isolated), test Stripe keys,
RESEND_DRY_RUN=1 so transactional email never actually sends.

Locked behind Cloudflare Access with team policy: anyone with
@neurontechnologies.ai email plus named external collaborators
via the access app's emails list.

Initial image is marketing:fix-gallery-render-1236 (matches stage
at the time of this commit). Future deploys land via CI workflow.
2026-05-02 13:13:58 -05:00
..

dev.neurontechnologies.ai

Team-internal free-fire iteration zone. Looser standards than stage: broken builds OK, no public users, no real PII, no live Stripe, no real outbound email. Auto-deploys on every push to the dev branch once the CI/CD runner agent ships (this directory provisions the shell + initial deploy only).

The terraform itself lives at servers/gcp/marketing-dev.tf (single file, mirrors the existing flat-file layout under servers/gcp/). This directory holds documentation only.

What is dev

Layer Where
Domain dev.neurontechnologies.ai
Cloud Run marketing-dev-us (us-central1, single region)
Image registry us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/
Initial image marketing:fix-gallery-render-1236 (matched stage at provision)
Service account neuron-marketing-dev@neuron-785695.iam.gserviceaccount.com
Access control Cloudflare Zero Trust Access, two policies
Identity rule 1 email_domain == neurontechnologies.ai
Identity rule 2 named externals via dev_external_collaborators variable
Supabase NEW dev project (NOT shared with stage or prod)
LLM claude-haiku-4-5 (cheap)
Stripe Test mode (same test keys as stage)
Email RESEND_API_KEY mounted; RESEND_DRY_RUN=1 short-circuits send

How dev differs from stage

Property stage dev
NODE_ENV staging development
LLM model claude-sonnet-4-5 claude-haiku-4-5
Supabase shares prod project dedicated neuron-dev project (isolated)
Email live (real sends) RESEND_DRY_RUN=1 (no real sends)
Stripe test mode test mode (same)
Auto-deploy manual every push to dev branch (post CI wire)
Allowed users team + Tim's gmail team + named external collaborators

How to get in

  1. Sign in with your @neurontechnologies.ai Google account at https://dev.neurontechnologies.ai/. Cloudflare Access intercepts the request, redirects you to the Google IdP, and only allows email_domain == neurontechnologies.ai plus any allowlisted externals through.
  2. After auth, you land on the running marketing site exactly as it would render in stage / prod, but with NODE_ENV=development.

Verify the lockdown from a shell at any time:

curl -sI https://dev.neurontechnologies.ai/ | head -2
# HTTP/2 302
# location: https://neuralplatform.cloudflareaccess.com/cdn-cgi/access/login/dev.neurontechnologies.ai?...

If the first line is anything other than 302 to the cloudflareaccess domain, something has broken. Treat it as a P0.

Provision the dev Supabase project (one-time, automated)

The Supabase Management API token lives in Vault at secret/neuron-technologies/supabase (field access_token).

# Pull the token
export SBP_TOKEN=$(vault kv get -field=access_token secret/neuron-technologies/supabase)

# Create the project
ORG_ID=$(curl -s -H "Authorization: Bearer $SBP_TOKEN" \
  https://api.supabase.com/v1/organizations | jq -r '.[0].id')

curl -s -X POST -H "Authorization: Bearer $SBP_TOKEN" \
  -H "Content-Type: application/json" \
  https://api.supabase.com/v1/projects \
  -d "{\"name\":\"neuron-dev\",\"region\":\"us-west-2\",\"organization_id\":\"$ORG_ID\",\"db_pass\":\"$(openssl rand -hex 24)\"}" | jq .

# Capture project ref, URL, anon_key, service_role_key from the response.

After provisioning, write the values into Secret Manager (this overwrites the placeholder versions terraform created):

echo -n 'https://<dev-ref>.supabase.co' | gcloud secrets versions add \
  dev-supabase-url --data-file=- --project=neuron-785695
echo -n '<dev-anon-key>' | gcloud secrets versions add \
  dev-supabase-anon-key --data-file=- --project=neuron-785695
echo -n '<dev-service-key>' | gcloud secrets versions add \
  dev-supabase-service-key --data-file=- --project=neuron-785695

Then apply the prod schema (5 tables + indexes + 1 backfill) to dev via the Supabase Management API SQL endpoint:

DEV_REF=<dev-ref>
for f in ~/Development/neuron-technologies/products/web/migrations/*.sql; do
  echo "=== applying $f ==="
  jq -Rs '{query: .}' < "$f" | curl -s -X POST \
    -H "Authorization: Bearer $SBP_TOKEN" \
    -H "Content-Type: application/json" \
    "https://api.supabase.com/v1/projects/$DEV_REF/database/query" \
    -d @-
done

Force a new dev revision so it picks up the secrets:

gcloud run services update marketing-dev-us --region=us-central1 \
  --project=neuron-785695 \
  --image=us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:fix-gallery-render-1236

Manual deploy (until CI is wired)

gcloud run services update marketing-dev-us \
  --region=us-central1 \
  --project=neuron-785695 \
  --image=us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:<tag>

The terraform google_cloud_run_v2_service.marketing_dev_us has lifecycle.ignore_changes = [template[0].containers[0].image] so CI rollouts will not be undone by the next terraform apply.

CI wiring lands once the CI/CD runner agent (commit a9bfb7d) ships. The follow-up backlog item is tracked in Neuron.

What NOT to do in dev

  • Do NOT use real PII. Anything entered here can be wiped without notice.
  • Do NOT use live Stripe keys. Test mode only.
  • Do NOT expect persistent data. Dev Supabase is disposable.
  • Do NOT ship live transactional email. RESEND_DRY_RUN=1 is the contract; if main.el's send_email helper does not honor it yet, the unverified dev.neurontechnologies.ai envelope-from will cause Resend to reject the send anyway. Either way: no real inbox is reached. Track that work via the Neuron backlog.
  • Do NOT grant the dev SA any prod IAM. Resource isolation is the only thing keeping dev experiments from corrupting prod.

Disaster recovery

Dev is disposable. If state gets weird, nuke it and rebuild:

cd ~/Development/infrastructure/servers/gcp
terraform destroy \
  -target=cloudflare_zero_trust_access_policy.marketing_dev_external \
  -target=cloudflare_zero_trust_access_policy.marketing_dev_team \
  -target=cloudflare_zero_trust_access_application.marketing_dev \
  -target=cloudflare_record.marketing_dev \
  -target=google_cloud_run_domain_mapping.marketing_dev \
  -target=google_cloud_run_v2_service_iam_member.marketing_dev_us_public \
  -target=google_cloud_run_v2_service.marketing_dev_us \
  -target=google_secret_manager_secret_version.dev_supabase_url_placeholder \
  -target=google_secret_manager_secret_version.dev_supabase_anon_key_placeholder \
  -target=google_secret_manager_secret_version.dev_supabase_service_key_placeholder \
  -target=google_secret_manager_secret_version.dev_stripe_price_professional_placeholder \
  -target=google_secret_manager_secret_version.dev_stripe_price_founding_placeholder \
  -target=google_secret_manager_secret_version.dev_stripe_price_family_child_placeholder \
  -target=google_secret_manager_secret.dev_supabase_url \
  -target=google_secret_manager_secret.dev_supabase_anon_key \
  -target=google_secret_manager_secret.dev_supabase_service_key \
  -target=google_secret_manager_secret.dev_stripe_price_professional \
  -target=google_secret_manager_secret.dev_stripe_price_founding \
  -target=google_secret_manager_secret.dev_stripe_price_family_child \
  -target=google_project_iam_member.marketing_dev_secret_accessor \
  -target=google_service_account.marketing_dev

# Then re-apply:
terraform apply

If the dev Supabase project was provisioned, also delete it from the Supabase dashboard and re-create with a new name. The Secret Manager secrets carry forward as placeholders; rewrite them with the new project's values.

Files

File What
servers/gcp/marketing-dev.tf All marketing-dev terraform
servers/gcp/marketing-dev/README.md This document