diff --git a/servers/gcp/cloud-run-stage.tf b/servers/gcp/cloud-run-stage.tf new file mode 100644 index 0000000..e4acaab --- /dev/null +++ b/servers/gcp/cloud-run-stage.tf @@ -0,0 +1,211 @@ +# ── Marketing Stage — us-central1 ───────────────────────────────────────────── +# Stage environment for the marketing site. Single region only (us-central1). +# Uses the :stage Docker image tag with test Stripe keys. +# Access is gated via Cloudflare Access (OAuth, @neurontechnologies.ai only). +# The Cloud Run direct URL is available but obscure; Cloudflare is the front door. + +resource "google_cloud_run_v2_service" "stage_us" { + name = "marketing-stage-us" + location = "us-central1" + project = var.project_id + + ingress = "INGRESS_TRAFFIC_ALL" + + labels = { + "managed-by" = "terraform" + "project" = "neuron-marketing" + "env" = "stage" + } + + template { + service_account = google_service_account.marketing.email + + scaling { + min_instance_count = 0 + max_instance_count = 5 + } + + containers { + image = "us-central1-docker.pkg.dev/${var.project_id}/neuron-marketing/marketing:stage" + + resources { + limits = { + cpu = "1" + memory = "512Mi" + } + cpu_idle = true + } + + env { + name = "NODE_ENV" + value = "staging" + } + env { + name = "STRIPE_PUBLISHABLE_KEY" + value = "pk_test_51TPoHwJak4gXxYZLPBDH5g53qOICo0Bzy4S6YYWw723rnD5ZGOfhoT5bIvB00o4gW47yknEz9SeQimUclrDT2txm00aYsPDech" + } + env { + name = "GCS_SHARE_BUCKET" + value = "neuron-shares-stage" + } + env { + name = "STRIPE_PRICE_FAMILY_CHILD" + value_source { + secret_key_ref { + secret = "stripe-price-family-child" + version = "latest" + } + } + } + env { + name = "NEURON_ORIGIN" + value = "https://stage.neurontechnologies.ai" + } + env { + name = "SUPABASE_ANON_KEY" + value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E" + } + + env { + name = "SUPABASE_SERVICE_KEY" + value_source { + secret_key_ref { + secret = "supabase-service-key" + version = "latest" + } + } + } + env { + name = "NEURON_LLM_0_FORMAT" + value = "anthropic" + } + env { + name = "NEURON_LLM_0_MODEL" + value = "claude-haiku-4-5" + } + env { + name = "NEURON_LLM_0_URL" + value = "https://api.anthropic.com/v1/messages" + } + env { + name = "NEURON_LLM_0_KEY" + value_source { + secret_key_ref { + secret = "anthropic-api-key" + version = "latest" + } + } + } + env { + name = "ANTHROPIC_API_KEY" + value_source { + secret_key_ref { + secret = "anthropic-api-key" + version = "latest" + } + } + } + env { + name = "STRIPE_SECRET_KEY" + value_source { + secret_key_ref { + secret = "stripe-secret-key-stage" + version = "latest" + } + } + } + env { + name = "STRIPE_WEBHOOK_SECRET" + value_source { + secret_key_ref { + secret = "stripe-webhook-secret-stage" + version = "latest" + } + } + } + env { + name = "STRIPE_PRICE_PROFESSIONAL" + value_source { + secret_key_ref { + secret = "stripe-price-professional-stage" + version = "latest" + } + } + } + env { + name = "STRIPE_PRICE_FOUNDING" + value_source { + secret_key_ref { + secret = "stripe-price-founding-stage" + version = "latest" + } + } + } + + ports { + container_port = 8080 + name = "http1" + } + + startup_probe { + http_get { + path = "/api/health" + port = 8080 + } + initial_delay_seconds = 15 + timeout_seconds = 5 + period_seconds = 10 + failure_threshold = 10 + } + + liveness_probe { + http_get { + path = "/api/health" + port = 8080 + } + initial_delay_seconds = 20 + timeout_seconds = 5 + period_seconds = 30 + failure_threshold = 3 + } + } + } + + traffic { + type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" + percent = 100 + } + + depends_on = [google_project_iam_member.marketing_secret_accessor] +} + +# Allow unauthenticated requests (Cloudflare Access is the gate, not Cloud Run auth) +resource "google_cloud_run_v2_service_iam_member" "stage_us_noauth" { + project = var.project_id + location = "us-central1" + name = google_cloud_run_v2_service.stage_us.name + role = "roles/run.invoker" + member = "allUsers" +} + +# Grant stage service account access to stage secrets +resource "google_secret_manager_secret_iam_member" "stage_stripe_key" { + secret_id = "stripe-secret-key-stage" + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.marketing.email}" +} +resource "google_secret_manager_secret_iam_member" "stage_webhook_secret" { + secret_id = "stripe-webhook-secret-stage" + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.marketing.email}" +} +resource "google_secret_manager_secret_iam_member" "stage_price_founding" { + secret_id = "stripe-price-founding-stage" + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.marketing.email}" +} +resource "google_secret_manager_secret_iam_member" "stage_price_professional" { + secret_id = "stripe-price-professional-stage" + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.marketing.email}" +} diff --git a/servers/gcp/cloud-run.tf b/servers/gcp/cloud-run.tf index 9f1f77c..950486b 100644 --- a/servers/gcp/cloud-run.tf +++ b/servers/gcp/cloud-run.tf @@ -48,11 +48,62 @@ resource "google_cloud_run_v2_service" "prod_us" { value = "production" } env { - name = "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" + name = "STRIPE_PUBLISHABLE_KEY" value = local.stripe_publishable_key } + env { + name = "GCS_SHARE_BUCKET" + value = "neuron-shares-prod" + } + env { + name = "SUPABASE_ANON_KEY" + value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E" + } + env { + name = "NEURON_ORIGIN" + value = "https://neurontechnologies.ai" + } + env { + name = "NEURON_LLM_0_FORMAT" + value = "anthropic" + } + env { + name = "NEURON_LLM_0_MODEL" + value = "claude-haiku-4-5" + } + env { + name = "NEURON_LLM_0_URL" + value = "https://api.anthropic.com/v1/messages" + } # ── Secret env vars from Secret Manager ─────────────────────────────── + env { + name = "SUPABASE_SERVICE_KEY" + value_source { + secret_key_ref { + secret = "supabase-service-key" + version = "latest" + } + } + } + env { + name = "NEURON_LLM_0_KEY" + value_source { + secret_key_ref { + secret = "anthropic-api-key" + version = "latest" + } + } + } + env { + name = "ANTHROPIC_API_KEY" + value_source { + secret_key_ref { + secret = "anthropic-api-key" + version = "latest" + } + } + } env { name = "STRIPE_SECRET_KEY" value_source { @@ -89,16 +140,34 @@ resource "google_cloud_run_v2_service" "prod_us" { } } } + env { + name = "STRIPE_PRICE_FAMILY_CHILD" + value_source { + secret_key_ref { + secret = "stripe-price-family-child" + version = "latest" + } + } + } + env { + name = "RESEND_API_KEY" + value_source { + secret_key_ref { + secret = "resend-api-key" + version = "latest" + } + } + } ports { - container_port = 3000 + container_port = 8080 name = "http1" } startup_probe { http_get { path = "/api/health" - port = 3000 + port = 8080 } initial_delay_seconds = 10 timeout_seconds = 5 @@ -109,7 +178,7 @@ resource "google_cloud_run_v2_service" "prod_us" { liveness_probe { http_get { path = "/api/health" - port = 3000 + port = 8080 } initial_delay_seconds = 15 timeout_seconds = 5 @@ -163,10 +232,61 @@ resource "google_cloud_run_v2_service" "prod_eu" { value = "production" } env { - name = "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" + name = "STRIPE_PUBLISHABLE_KEY" value = local.stripe_publishable_key } + env { + name = "GCS_SHARE_BUCKET" + value = "neuron-shares-prod" + } + env { + name = "SUPABASE_ANON_KEY" + value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E" + } + env { + name = "NEURON_ORIGIN" + value = "https://neurontechnologies.ai" + } + env { + name = "NEURON_LLM_0_FORMAT" + value = "anthropic" + } + env { + name = "NEURON_LLM_0_MODEL" + value = "claude-haiku-4-5" + } + env { + name = "NEURON_LLM_0_URL" + value = "https://api.anthropic.com/v1/messages" + } + env { + name = "SUPABASE_SERVICE_KEY" + value_source { + secret_key_ref { + secret = "supabase-service-key" + version = "latest" + } + } + } + env { + name = "NEURON_LLM_0_KEY" + value_source { + secret_key_ref { + secret = "anthropic-api-key" + version = "latest" + } + } + } + env { + name = "ANTHROPIC_API_KEY" + value_source { + secret_key_ref { + secret = "anthropic-api-key" + version = "latest" + } + } + } env { name = "STRIPE_SECRET_KEY" value_source { @@ -203,16 +323,34 @@ resource "google_cloud_run_v2_service" "prod_eu" { } } } + env { + name = "STRIPE_PRICE_FAMILY_CHILD" + value_source { + secret_key_ref { + secret = "stripe-price-family-child" + version = "latest" + } + } + } + env { + name = "RESEND_API_KEY" + value_source { + secret_key_ref { + secret = "resend-api-key" + version = "latest" + } + } + } ports { - container_port = 3000 + container_port = 8080 name = "http1" } startup_probe { http_get { path = "/api/health" - port = 3000 + port = 8080 } initial_delay_seconds = 10 timeout_seconds = 5 @@ -223,7 +361,7 @@ resource "google_cloud_run_v2_service" "prod_eu" { liveness_probe { http_get { path = "/api/health" - port = 3000 + port = 8080 } initial_delay_seconds = 15 timeout_seconds = 5 @@ -277,10 +415,61 @@ resource "google_cloud_run_v2_service" "prod_apac" { value = "production" } env { - name = "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" + name = "STRIPE_PUBLISHABLE_KEY" value = local.stripe_publishable_key } + env { + name = "GCS_SHARE_BUCKET" + value = "neuron-shares-prod" + } + env { + name = "SUPABASE_ANON_KEY" + value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E" + } + env { + name = "NEURON_ORIGIN" + value = "https://neurontechnologies.ai" + } + env { + name = "NEURON_LLM_0_FORMAT" + value = "anthropic" + } + env { + name = "NEURON_LLM_0_MODEL" + value = "claude-haiku-4-5" + } + env { + name = "NEURON_LLM_0_URL" + value = "https://api.anthropic.com/v1/messages" + } + env { + name = "SUPABASE_SERVICE_KEY" + value_source { + secret_key_ref { + secret = "supabase-service-key" + version = "latest" + } + } + } + env { + name = "NEURON_LLM_0_KEY" + value_source { + secret_key_ref { + secret = "anthropic-api-key" + version = "latest" + } + } + } + env { + name = "ANTHROPIC_API_KEY" + value_source { + secret_key_ref { + secret = "anthropic-api-key" + version = "latest" + } + } + } env { name = "STRIPE_SECRET_KEY" value_source { @@ -317,16 +506,34 @@ resource "google_cloud_run_v2_service" "prod_apac" { } } } + env { + name = "STRIPE_PRICE_FAMILY_CHILD" + value_source { + secret_key_ref { + secret = "stripe-price-family-child" + version = "latest" + } + } + } + env { + name = "RESEND_API_KEY" + value_source { + secret_key_ref { + secret = "resend-api-key" + version = "latest" + } + } + } ports { - container_port = 3000 + container_port = 8080 name = "http1" } startup_probe { http_get { path = "/api/health" - port = 3000 + port = 8080 } initial_delay_seconds = 10 timeout_seconds = 5 @@ -337,7 +544,7 @@ resource "google_cloud_run_v2_service" "prod_apac" { liveness_probe { http_get { path = "/api/health" - port = 3000 + port = 8080 } initial_delay_seconds = 15 timeout_seconds = 5 @@ -356,4 +563,3 @@ resource "google_cloud_run_v2_service" "prod_apac" { depends_on = [google_project_iam_member.marketing_secret_accessor] } - diff --git a/servers/gcp/storage.tf b/servers/gcp/storage.tf new file mode 100644 index 0000000..4a784dc --- /dev/null +++ b/servers/gcp/storage.tf @@ -0,0 +1,96 @@ +# ── Cloud Storage ───────────────────────────────────────────────────────────── +# Share card blobs from the marketing landing page demo chat. +# Separate buckets for prod and stage/test. +# Each blob is a small JSON file (~1KB) keyed by share ID. +# Public read so the landing server can serve card pages. +# Marketing SA has write access to upload new cards. + +locals { + share_bucket_labels = { + "managed-by" = "terraform" + "project" = "neuron-marketing" + "purpose" = "share-cards" + } +} + +# ── Prod bucket ─────────────────────────────────────────────────────────────── + +resource "google_storage_bucket" "shares_prod" { + name = "neuron-shares-prod" + project = var.project_id + location = "US" + storage_class = "STANDARD" + + uniform_bucket_level_access = true + + cors { + origin = ["https://neurontechnologies.ai"] + method = ["GET"] + response_header = ["Content-Type"] + max_age_seconds = 3600 + } + + lifecycle_rule { + action { + type = "Delete" + } + condition { + age = 365 + } + } + + labels = merge(local.share_bucket_labels, { "env" = "prod" }) +} + +resource "google_storage_bucket_iam_member" "shares_prod_public_read" { + bucket = google_storage_bucket.shares_prod.name + role = "roles/storage.objectViewer" + member = "allUsers" +} + +resource "google_storage_bucket_iam_member" "shares_prod_marketing_write" { + bucket = google_storage_bucket.shares_prod.name + role = "roles/storage.objectUser" + member = "serviceAccount:${google_service_account.marketing.email}" +} + +# ── Stage / test bucket ─────────────────────────────────────────────────────── + +resource "google_storage_bucket" "shares_stage" { + name = "neuron-shares-stage" + project = var.project_id + location = "US" + storage_class = "STANDARD" + + uniform_bucket_level_access = true + + cors { + origin = ["https://stage.neurontechnologies.ai", "http://localhost:3001"] + method = ["GET"] + response_header = ["Content-Type"] + max_age_seconds = 3600 + } + + lifecycle_rule { + action { + type = "Delete" + } + condition { + age = 30 + } + } + + labels = merge(local.share_bucket_labels, { "env" = "stage" }) +} + +resource "google_storage_bucket_iam_member" "shares_stage_public_read" { + bucket = google_storage_bucket.shares_stage.name + role = "roles/storage.objectViewer" + member = "allUsers" +} + +resource "google_storage_bucket_iam_member" "shares_stage_marketing_write" { + bucket = google_storage_bucket.shares_stage.name + role = "roles/storage.objectUser" + member = "serviceAccount:${google_service_account.marketing.email}" +}