diff --git a/internal/localmode/assets/docker-compose.template.yml b/internal/localmode/assets/docker-compose.template.yml index d7f7a11..5e40a54 100644 --- a/internal/localmode/assets/docker-compose.template.yml +++ b/internal/localmode/assets/docker-compose.template.yml @@ -82,9 +82,9 @@ services: AWS_REGION: us-east-1 AWS_ACCESS_KEY_ID: local AWS_SECRET_ACCESS_KEY: local - REDIS_TIMEOUT: "60" - USAGE_SYNC_INTERVAL: "30" - USAGE_SYNC_LOCK_TTL: "30" + REDIS_TIMEOUT: "60s" + USAGE_SYNC_INTERVAL: "30s" + USAGE_SYNC_LOCK_TTL: "30s" SOURCE_ARCHIVE_SIZE_LIMIT_MB: "256" LAMBDA_TARGET_CONTAINER_SIZE_LIMIT_MB: "4096" PORT: "8000" @@ -104,48 +104,65 @@ services: FREE_FUNCTION_TIMEOUT: "30" FREE_FUNCTION_MEMORY: "128" FREE_FUNCTION_DISK: "512" - PRO_FUNCTION_TIMEOUT: "300" - PRO_FUNCTION_MEMORY: "1024" - PRO_FUNCTION_DISK: "2048" + FREE_FUNCTION_RATELIMIT: "10" + FREE_FUNCTION_ALL_RATELIMIT: "60" + FREE_FUNC_INVOCATIONS_PER_MONTH: "100000" + FREE_BUILD_TIMEOUT_MINUTES: "30" + # Monthly build-minutes cap (0 = unlimited). Local-mode keeps builds + # unconstrained so dev iteration is never blocked. + FREE_BUILD_MAX_MINUTES: "0" + PRO_BUILD_MAX_MINUTES: "0" + # Runtime log retention (TTL) and max search lookback in days. + FREE_LOG_RETENTION_DAYS: "1" + PRO_LOG_RETENTION_DAYS: "30" + PRO_FUNCTION_TIMEOUT: "180" + PRO_FUNCTION_MEMORY: "256" + PRO_FUNCTION_DISK: "1024" + PRO_FUNCTION_RATELIMIT: "0" + PRO_FUNCTION_ALL_RATELIMIT: "0" + PRO_FUNC_INVOCATIONS_PER_MONTH: "0" + PRO_BUILD_TIMEOUT_MINUTES: "60" + FREE_IMAGE_OPTIMIZER_TIMEOUT: "90" + PRO_IMAGE_OPTIMIZER_TIMEOUT: "90" + FREE_IMAGE_OPTIMIZER_MEMORY: "1024" + PRO_IMAGE_OPTIMIZER_MEMORY: "1024" + FREE_IMAGE_OPTIMIZER_DISK: "1024" + PRO_IMAGE_OPTIMIZER_DISK: "1024" # Custom-domain plan limits (cloud separates FREE/PRO; meaningless locally) FREE_FRONTEND_CUSTOM_DOMAINS: "10" PRO_FRONTEND_CUSTOM_DOMAINS: "10" - # Function scheduler limits (cloud: FREE=0, PRO=5; local-mode keeps both - # permissive so signup users on the default FREE plan can still test) - FREE_SCHEDULER_COUNT: "10" - PRO_SCHEDULER_COUNT: "10" + # Function scheduler limits. Sentinel convention: -1 = disabled, 0 = + # unlimited, N = cap. Cloud sets FREE=-1 (disabled), PRO=5; local-mode + # keeps cloud parity for plan-limit testing. + FREE_SCHEDULER_COUNT: "-1" + PRO_SCHEDULER_COUNT: "5" + # Database count caps. Sentinel convention: 0 = unlimited, N = cap. Cloud + # sets FREE=1, PRO=0 (unlimited); local-mode keeps both unlimited so signup + # users on the default FREE plan can create as many databases as they need. + FREE_DATABASE_CAP: "0" + PRO_DATABASE_CAP: "0" + # Database storage caps in MB. 0 = unlimited. Cloud sets FREE=1024 (1 GB), + # PRO=0; local-mode keeps both unlimited so dev databases can grow freely. + FREE_DATABASE_STORAGE_LIMIT_MB: "0" + PRO_DATABASE_STORAGE_LIMIT_MB: "0" + FREE_STORAGE_BUCKETS_PER_PROJECT: "20" + PRO_STORAGE_BUCKETS_PER_PROJECT: "0" # Deployable regions used by project region resolution (`all_regions=true` # projects default to this list). Cloud passes the real multi-region set; # locally we surface a representative subset so feature pickers and # scheduler validation have something to work with. AWS_REGIONS: "us-east-1,eu-west-1,ap-southeast-1" - # Stripe Billing (optional) - STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-} - STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-} - STRIPE_PRICE_ID_PRO_MONTHLY: ${STRIPE_PRICE_ID_PRO_MONTHLY:-} - STRIPE_PRICE_ID_PRO_YEARLY: ${STRIPE_PRICE_ID_PRO_YEARLY:-} - STRIPE_PRICE_ID_ADDON_BUILDER_CREDITS_MONTHLY: ${STRIPE_PRICE_ID_ADDON_BUILDER_CREDITS_MONTHLY:-} - STRIPE_PRICE_ID_ADDON_BUILDER_CREDITS_YEARLY: ${STRIPE_PRICE_ID_ADDON_BUILDER_CREDITS_YEARLY:-} - STRIPE_PRICE_ID_ADDON_FUNC_INVOCATIONS_MONTHLY: ${STRIPE_PRICE_ID_ADDON_FUNC_INVOCATIONS_MONTHLY:-} - STRIPE_PRICE_ID_ADDON_FUNC_INVOCATIONS_YEARLY: ${STRIPE_PRICE_ID_ADDON_FUNC_INVOCATIONS_YEARLY:-} - STRIPE_PRICE_ID_ADDON_STORAGE_REQUESTS_MONTHLY: ${STRIPE_PRICE_ID_ADDON_STORAGE_REQUESTS_MONTHLY:-} - STRIPE_PRICE_ID_ADDON_STORAGE_REQUESTS_YEARLY: ${STRIPE_PRICE_ID_ADDON_STORAGE_REQUESTS_YEARLY:-} - STRIPE_PRICE_ID_ADDON_AUTH_REQUESTS_MONTHLY: ${STRIPE_PRICE_ID_ADDON_AUTH_REQUESTS_MONTHLY:-} - STRIPE_PRICE_ID_ADDON_AUTH_REQUESTS_YEARLY: ${STRIPE_PRICE_ID_ADDON_AUTH_REQUESTS_YEARLY:-} - STRIPE_PRICE_ID_ADDON_DATABASE_REQUESTS_MONTHLY: ${STRIPE_PRICE_ID_ADDON_DATABASE_REQUESTS_MONTHLY:-} - STRIPE_PRICE_ID_ADDON_DATABASE_REQUESTS_YEARLY: ${STRIPE_PRICE_ID_ADDON_DATABASE_REQUESTS_YEARLY:-} - STRIPE_PRICE_ID_ADDON_REALTIME_MESSAGES_MONTHLY: ${STRIPE_PRICE_ID_ADDON_REALTIME_MESSAGES_MONTHLY:-} - STRIPE_PRICE_ID_ADDON_REALTIME_MESSAGES_YEARLY: ${STRIPE_PRICE_ID_ADDON_REALTIME_MESSAGES_YEARLY:-} - STRIPE_PRICE_ID_OVERAGE_FUNC_INVOCATIONS_MONTHLY: ${STRIPE_PRICE_ID_OVERAGE_FUNC_INVOCATIONS_MONTHLY:-} - STRIPE_PRICE_ID_OVERAGE_FUNC_INVOCATIONS_YEARLY: ${STRIPE_PRICE_ID_OVERAGE_FUNC_INVOCATIONS_YEARLY:-} - STRIPE_PRICE_ID_OVERAGE_STORAGE_REQUESTS_MONTHLY: ${STRIPE_PRICE_ID_OVERAGE_STORAGE_REQUESTS_MONTHLY:-} - STRIPE_PRICE_ID_OVERAGE_STORAGE_REQUESTS_YEARLY: ${STRIPE_PRICE_ID_OVERAGE_STORAGE_REQUESTS_YEARLY:-} - STRIPE_PRICE_ID_OVERAGE_AUTH_REQUESTS_MONTHLY: ${STRIPE_PRICE_ID_OVERAGE_AUTH_REQUESTS_MONTHLY:-} - STRIPE_PRICE_ID_OVERAGE_AUTH_REQUESTS_YEARLY: ${STRIPE_PRICE_ID_OVERAGE_AUTH_REQUESTS_YEARLY:-} - STRIPE_PRICE_ID_OVERAGE_DATABASE_REQUESTS_MONTHLY: ${STRIPE_PRICE_ID_OVERAGE_DATABASE_REQUESTS_MONTHLY:-} - STRIPE_PRICE_ID_OVERAGE_DATABASE_REQUESTS_YEARLY: ${STRIPE_PRICE_ID_OVERAGE_DATABASE_REQUESTS_YEARLY:-} - STRIPE_PRICE_ID_OVERAGE_REALTIME_MESSAGES_MONTHLY: ${STRIPE_PRICE_ID_OVERAGE_REALTIME_MESSAGES_MONTHLY:-} - STRIPE_PRICE_ID_OVERAGE_REALTIME_MESSAGES_YEARLY: ${STRIPE_PRICE_ID_OVERAGE_REALTIME_MESSAGES_YEARLY:-} + # First-party bootstrap (optional). ANON_KEY_SECRET is sourced from + # .env.local and must match the secret VOLCANO_FIRST_PARTY_ANON_KEY was + # signed with, since the server validates that key during bootstrap. + ANON_KEY_SECRET: ${ANON_KEY_SECRET:-} + VOLCANO_FIRST_PARTY_USER_ID: ${VOLCANO_FIRST_PARTY_USER_ID:-} + VOLCANO_FIRST_PARTY_USER_DISPLAY_NAME: ${VOLCANO_FIRST_PARTY_USER_DISPLAY_NAME:-} + VOLCANO_FIRST_PARTY_USER_TOKEN: ${VOLCANO_FIRST_PARTY_USER_TOKEN:-} + VOLCANO_FIRST_PARTY_PROJECT_ID: ${VOLCANO_FIRST_PARTY_PROJECT_ID:-} + VOLCANO_FIRST_PARTY_PROJECT_NAME: ${VOLCANO_FIRST_PARTY_PROJECT_NAME:-} + VOLCANO_FIRST_PARTY_ANON_KEY: ${VOLCANO_FIRST_PARTY_ANON_KEY:-} + VOLCANO_FIRST_PARTY_DEVICE_CLIENT_ID: ${VOLCANO_FIRST_PARTY_DEVICE_CLIENT_ID:-} volumes: - volcano-storage:/app/local-storage ports: diff --git a/internal/localmode/compose_test.go b/internal/localmode/compose_test.go index bf3e925..d4e94a4 100644 --- a/internal/localmode/compose_test.go +++ b/internal/localmode/compose_test.go @@ -95,10 +95,22 @@ func TestComposeEnvironmentDefaultsVolcanoImage(t *testing.T) { func TestDockerComposeTemplateLeavesServerOwnedLocalSecretsUnset(t *testing.T) { template := string(dockerComposeTemplate) + // The local server generates and owns these secrets, so they must never + // appear in the template at all. assert.NotContains(t, template, "JWT_SECRET:") assert.NotContains(t, template, "ENCRYPTION_KEY:") - assert.NotContains(t, template, "ANON_KEY_SECRET:") assert.NotContains(t, template, "SERVICE_KEY_SECRET:") + + // ANON_KEY_SECRET is an optional first-party-bootstrap passthrough. It may + // appear only as an empty ${ANON_KEY_SECRET:-} default (which leaves it unset + // so the server still owns it), never as a hardcoded secret value. + for line := range strings.SplitSeq(template, "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "ANON_KEY_SECRET:") { + assert.Equal(t, "ANON_KEY_SECRET: ${ANON_KEY_SECRET:-}", trimmed, + "ANON_KEY_SECRET must only be an empty passthrough, not a hardcoded secret") + } + } } func TestDockerComposeTemplateExposesLocalFrontendProxy(t *testing.T) { diff --git a/internal/localmode/info_test.go b/internal/localmode/info_test.go index 71f0e5e..f4de60c 100644 --- a/internal/localmode/info_test.go +++ b/internal/localmode/info_test.go @@ -30,7 +30,7 @@ func TestFetchInfoRunsDockerExecLocalInfo(t *testing.T) { "default_database_name":"app", "default_database_region":"local", "default_database_postgres_version":"16", - "database_url":"postgres://volcano:volcano@localhost:8002/app?sslmode=disable&application_name=volcano_full_access:app", + "database_url":"postgres://volcano_client_22222222-2222-2222-2222-222222222222:vpg_local_secret@localhost:8002/app?sslmode=disable&application_name=volcano_full_access", "redis_url":"redis://localhost:6379", "jwt_secret":"server-owned-jwt-secret", "encryption_key":"server-owned-encryption-key", @@ -54,7 +54,7 @@ func TestFetchInfoRunsDockerExecLocalInfo(t *testing.T) { assert.Equal(t, "app", info.DefaultDatabaseName) assert.Equal(t, "local", info.DefaultDatabaseRegion) assert.Equal(t, "16", info.DefaultDatabasePostgresVersion) - assert.Equal(t, "postgres://volcano:volcano@localhost:8002/app?sslmode=disable&application_name=volcano_full_access:app", info.DatabaseURL) + assert.Equal(t, "postgres://volcano_client_22222222-2222-2222-2222-222222222222:vpg_local_secret@localhost:8002/app?sslmode=disable&application_name=volcano_full_access", info.DatabaseURL) assert.Equal(t, "redis://localhost:6379", info.RedisURL) assert.Equal(t, "server-owned-jwt-secret", info.JWTSecret) assert.Equal(t, "server-owned-encryption-key", info.EncryptionKey)