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.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
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`).
|
||||
|
||||
```bash
|
||||
# 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):
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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)
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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 |
|
||||
Reference in New Issue
Block a user