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.
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) |
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) |
| 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
- Sign in with your
@neurontechnologies.aiGoogle account athttps://dev.neurontechnologies.ai/. Cloudflare Access intercepts the request, redirects you to the Google IdP, and only allowsemail_domain == neurontechnologies.aiplus any allowlisted externals through. - 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=1is the contract; ifmain.el'ssend_emailhelper does not honor it yet, the unverifieddev.neurontechnologies.aienvelope-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 |