belt 0.2.17 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +226 -0
  3. data/README.md +42 -3
  4. data/lib/belt/action_router.rb +34 -19
  5. data/lib/belt/cli/app_detection.rb +30 -3
  6. data/lib/belt/cli/auth_command.rb +395 -0
  7. data/lib/belt/cli/console_command.rb +17 -2
  8. data/lib/belt/cli/deploy_command.rb +51 -17
  9. data/lib/belt/cli/destroy_command.rb +62 -25
  10. data/lib/belt/cli/doctor_command.rb +166 -22
  11. data/lib/belt/cli/environment_command.rb +12 -0
  12. data/lib/belt/cli/explain_command.rb +112 -0
  13. data/lib/belt/cli/frontend_command.rb +31 -10
  14. data/lib/belt/cli/frontend_deploy_command.rb +110 -21
  15. data/lib/belt/cli/frontend_env_command.rb +56 -19
  16. data/lib/belt/cli/frontend_env_map.rb +30 -10
  17. data/lib/belt/cli/frontend_registry.rb +373 -0
  18. data/lib/belt/cli/frontend_setup_command.rb +71 -5
  19. data/lib/belt/cli/generate_command.rb +61 -42
  20. data/lib/belt/cli/index_command.rb +192 -0
  21. data/lib/belt/cli/lambda_config_command.rb +9 -3
  22. data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
  23. data/lib/belt/cli/routes_command/route_inference.rb +4 -3
  24. data/lib/belt/cli/routes_command.rb +31 -19
  25. data/lib/belt/cli/server_command.rb +28 -15
  26. data/lib/belt/cli/tables_command.rb +3 -1
  27. data/lib/belt/cli/terraform_command.rb +9 -0
  28. data/lib/belt/cli/views_command.rb +15 -15
  29. data/lib/belt/cli/zip_artifact_builder.rb +199 -0
  30. data/lib/belt/cli.rb +17 -9
  31. data/lib/belt/docs/backups.md +101 -0
  32. data/lib/belt/docs/console.md +65 -0
  33. data/lib/belt/docs/controllers.md +155 -0
  34. data/lib/belt/docs/deployment.md +166 -0
  35. data/lib/belt/docs/frontend.md +103 -0
  36. data/lib/belt/docs/generators.md +137 -0
  37. data/lib/belt/docs/lambda_handler.md +105 -0
  38. data/lib/belt/docs/models.md +160 -0
  39. data/lib/belt/docs/observability.md +94 -0
  40. data/lib/belt/docs/plugins.md +94 -0
  41. data/lib/belt/docs/routing.md +138 -0
  42. data/lib/belt/docs/structure.md +96 -0
  43. data/lib/belt/inflector.rb +8 -0
  44. data/lib/belt/route_dsl.rb +318 -100
  45. data/lib/belt/version.rb +1 -1
  46. data/lib/templates/frontend/react/env.yml.example +2 -2
  47. data/lib/templates/generate/auth/cognito.tf.erb +71 -0
  48. data/lib/templates/generate/auth/cognito_outputs.tf.erb +19 -0
  49. data/lib/templates/generate/auth/frontend/ConfirmEmail.jsx +55 -0
  50. data/lib/templates/generate/auth/frontend/Login.jsx +79 -0
  51. data/lib/templates/generate/auth/frontend/ProtectedRoute.jsx +9 -0
  52. data/lib/templates/generate/auth/frontend/SignUp.jsx +58 -0
  53. data/lib/templates/generate/auth/frontend/apiClient.js +34 -0
  54. data/lib/templates/generate/auth/frontend/auth.css +157 -0
  55. data/lib/templates/generate/auth/frontend/auth.js +121 -0
  56. data/lib/templates/generate/model.rb.erb +4 -6
  57. data/lib/templates/module/frontend.tf.erb +49 -43
  58. data/lib/templates/module/outputs.tf.erb +1 -1
  59. data/lib/templates/new_app/AGENTS.md.erb +170 -20
  60. data/lib/templates/new_app/config/lambda/api.yml.erb +1 -0
  61. data/lib/templates/new_app/config/routes.rb.erb +8 -3
  62. data/lib/templates/new_app/lambda/api.rb.erb +1 -1
  63. metadata +56 -1
@@ -0,0 +1,101 @@
1
+ # Backups
2
+
3
+ Belt integrates automated pre-deploy backups into the deploy lifecycle.
4
+ When configured, `belt deploy` creates recovery points before applying changes.
5
+
6
+ ## Quick Setup
7
+
8
+ Create `infrastructure/<env>/belt.rb`:
9
+
10
+ ```ruby
11
+ Belt.configure do |config|
12
+ config.backups do
13
+ dynamodb :all
14
+ retention snapshots: 90
15
+ end
16
+ end
17
+ ```
18
+
19
+ Then deploy normally — backups run automatically:
20
+
21
+ ```bash
22
+ belt deploy prod
23
+ ```
24
+
25
+ ## Simple Mode
26
+
27
+ For DynamoDB-only backups with defaults (all tables, 90-day retention):
28
+
29
+ ```ruby
30
+ Belt.configure do |config|
31
+ config.backups = true
32
+ end
33
+ ```
34
+
35
+ ## Full Configuration
36
+
37
+ ```ruby
38
+ Belt.configure do |config|
39
+ config.backups do
40
+ dynamodb :all # All tables: PITR check + on-demand snapshot
41
+ dynamodb :posts, :users # Or specific tables only
42
+ cognito :users, :pool_config # Export user list + pool settings to S3
43
+ s3 :legal_documents # Sync bucket to backup bucket
44
+ retention snapshots: 90, cognito: 10, s3: 10
45
+ end
46
+ end
47
+ ```
48
+
49
+ ## Backup Types
50
+
51
+ | Type | What It Does | Default Retention |
52
+ |------|-------------|-------------------|
53
+ | `dynamodb :all` | PITR verification + on-demand snapshot per table | 90 days |
54
+ | `dynamodb :table1, :table2` | Same, specific tables only | 90 days |
55
+ | `cognito :users` | Paginated user export → JSON in backup bucket | 10 copies |
56
+ | `cognito :pool_config` | Pool configuration export → JSON | 10 copies |
57
+ | `s3 :bucket_name` | Full sync to backup bucket | 10 copies |
58
+
59
+ ## CLI Flags
60
+
61
+ ```bash
62
+ belt deploy prod # normal deploy (runs backups first)
63
+ belt deploy prod --skip-backup # skip backup phase
64
+ belt deploy prod --backup-only # just create recovery point, don't deploy
65
+ ```
66
+
67
+ ## How It Works
68
+
69
+ 1. Creates backup bucket `<app-name>-backups-<env>` on first run (versioned, public access blocked)
70
+ 2. Reads table names from `terraform output`
71
+ 3. Verifies PITR is enabled on each DynamoDB table
72
+ 4. Creates on-demand backup named `<table>-<timestamp>`
73
+ 5. For Cognito/S3: exports to backup bucket under timestamped prefixes
74
+ 6. Cleans up expired snapshots/copies beyond retention
75
+
76
+ ## First Deploy
77
+
78
+ On a brand-new environment with no prior deploys, there are no Terraform outputs
79
+ to read table names from. Belt warns and skips the backup phase gracefully.
80
+ After the first successful deploy, backups run normally.
81
+
82
+ ## DynamoDB Protection Defaults
83
+
84
+ All Belt-generated DynamoDB tables include:
85
+ - **PITR** (Point-in-Time Recovery) — enabled by default (35 days continuous)
86
+ - **Deletion protection** — enabled in prod, disabled in dev
87
+
88
+ ## Skipping Backups in Dev
89
+
90
+ Don't create `infrastructure/dev01/belt.rb`, or omit the backups block:
91
+
92
+ ```ruby
93
+ Belt.configure do |config|
94
+ # No backups block = no backups during deploy
95
+ end
96
+ ```
97
+
98
+ ## See Also
99
+
100
+ - `belt explain deployment` — the full deploy lifecycle
101
+ - `belt deploy --help` — all deploy options
@@ -0,0 +1,65 @@
1
+ # Console
2
+
3
+ `belt console` (alias: `belt c`) starts an interactive Ruby session with your
4
+ application fully loaded — models, configuration, AWS clients, everything.
5
+
6
+ ## Basic Usage
7
+
8
+ ```bash
9
+ belt console # uses BELT_ENV or defaults to 'dev'
10
+ belt c prod # specify environment
11
+ belt c dev01 # any environment name
12
+ ```
13
+
14
+ ## What Gets Loaded
15
+
16
+ 1. `lambda/config/environment.rb` — your app's boot file (AWS setup, models, libs)
17
+ 2. IRB starts with `reload!` available
18
+ 3. `.irbrc` from project root (optional console customization)
19
+
20
+ ## Runner Mode
21
+
22
+ Execute a command and exit (useful for scripts/CI):
23
+
24
+ ```bash
25
+ belt c dev01 --run "Customer.first"
26
+ belt c prod --run "Post.count"
27
+ belt c dev01 --run "User.where(status: 'active', index: 'StatusIndex').count"
28
+ ```
29
+
30
+ ## Production Safety
31
+
32
+ When the environment is `prod`, Belt shows a confirmation prompt before
33
+ starting the console. This prevents accidentally running destructive commands
34
+ against production data.
35
+
36
+ ## Common Tasks
37
+
38
+ ```ruby
39
+ # Find a record
40
+ post = Post.find("post-id-123")
41
+
42
+ # Query with index
43
+ users = User.where(status: "active", index: "StatusIndex")
44
+
45
+ # Create a record
46
+ Post.create!(title: "Test", body: "Hello", user_id: "u-123")
47
+
48
+ # Count records
49
+ Order.count
50
+
51
+ # Reload code changes
52
+ reload!
53
+ ```
54
+
55
+ ## Environment Resolution
56
+
57
+ Priority order:
58
+ 1. Explicit argument: `belt c prod`
59
+ 2. `BELT_ENV` environment variable
60
+ 3. Default: `dev`
61
+
62
+ ## See Also
63
+
64
+ - `belt explain models` — ActiveItem query methods
65
+ - `belt explain structure` — where environment.rb lives
@@ -0,0 +1,155 @@
1
+ # Controllers
2
+
3
+ Belt controllers inherit from `BeltController::Base` and handle HTTP requests
4
+ dispatched by `Belt::ActionRouter`. They provide callbacks, strong parameters,
5
+ response helpers, and error handling — similar to Rails ActionController.
6
+
7
+ ## Basic Controller
8
+
9
+ ```ruby
10
+ module MyApp
11
+ class PostsController < ApplicationController
12
+ def index
13
+ @posts = Post.all
14
+ end
15
+
16
+ def show
17
+ @post = Post.find(params["id"])
18
+ end
19
+
20
+ def create
21
+ attrs = params.require(:post).permit(:title, :body).to_h
22
+ @post = Post.create!(attrs.merge(user_id: current_user_id))
23
+ response_status :created
24
+ end
25
+
26
+ def destroy
27
+ Post.find(params["id"]).destroy
28
+ head :no_content
29
+ end
30
+ end
31
+ end
32
+ ```
33
+
34
+ ## Response Behavior
35
+
36
+ ### Implicit Responses (default: JSON)
37
+
38
+ When an action sets instance variables and returns without calling a response
39
+ helper, Belt auto-serializes assigns into a JSON response:
40
+
41
+ ```ruby
42
+ def index
43
+ @posts = Post.all # → { "posts": [...] }
44
+ end
45
+
46
+ def show
47
+ @post = Post.find(params["id"]) # → { "post": {...} }
48
+ end
49
+ ```
50
+
51
+ ### Explicit Response Helpers
52
+
53
+ ```ruby
54
+ success_response({ id: "123", name: "Example" }) # 200 JSON
55
+ success_response({ id: "123" }, :created) # 201 JSON
56
+ error_response("Not found", :not_found) # 404 JSON
57
+ error_response("Nope", :unprocessable_entity) # 422 JSON
58
+ html_response("<h1>Hello</h1>") # 200 HTML
59
+ head :no_content # 204 empty
60
+ head :created # 201 empty
61
+ ```
62
+
63
+ ### Non-200 with Implicit Assigns
64
+
65
+ ```ruby
66
+ def create
67
+ @post = Post.create!(...)
68
+ response_status :created # → 201 + { "post": {...} }
69
+ end
70
+ ```
71
+
72
+ ### Default Format
73
+
74
+ ```ruby
75
+ # Global (in config/environment.rb)
76
+ Belt.configure do |config|
77
+ config.default_format = :json # or :html
78
+ end
79
+
80
+ # Per-controller
81
+ class PagesController < ApplicationController
82
+ self.default_format = :html
83
+ end
84
+ ```
85
+
86
+ - `:json` — assigns become JSON body
87
+ - `:html` — renders `views/<controller>/<action>.html.erb`
88
+
89
+ ## Callbacks
90
+
91
+ ```ruby
92
+ class ApplicationController < BeltController::Base
93
+ before_action :authenticate!
94
+ before_action :require_admin!, except: [:health]
95
+ skip_before_action :authenticate!, only: [:health]
96
+ end
97
+ ```
98
+
99
+ Callbacks run in definition order. `before_action` can halt the request by
100
+ calling a response helper (e.g., `error_response`).
101
+
102
+ ## Strong Parameters
103
+
104
+ ```ruby
105
+ params.require(:user).permit(:name, :email, address: [:street, :city])
106
+ ```
107
+
108
+ - `params` — merged hash of path parameters + parsed JSON body
109
+ - `require(:key)` — raises if key missing
110
+ - `permit(:field1, :field2)` — whitelists allowed fields
111
+ - Nested: `permit(:name, address: [:street, :city])`
112
+
113
+ ## Error Handling
114
+
115
+ ```ruby
116
+ class ApplicationController < BeltController::Base
117
+ rescue_from ActiveItem::RecordNotFound, with: :not_found
118
+ rescue_from MyCustomError, with: :handle_custom
119
+
120
+ private
121
+
122
+ def not_found(exception, _context = {})
123
+ error_response(exception.message, :not_found)
124
+ end
125
+
126
+ def handle_custom(exception, _context = {})
127
+ error_response(exception.message, :unprocessable_entity)
128
+ end
129
+ end
130
+ ```
131
+
132
+ ## Controller Discovery
133
+
134
+ Belt resolves controllers by:
135
+ 1. Checking the app's namespace module (e.g., `MyApp::PostsController`)
136
+ 2. Searching `Belt.all_controller_paths`
137
+
138
+ No manual registration required. Controllers are auto-discovered from the
139
+ `lambda/controllers/` directory.
140
+
141
+ ## CORS
142
+
143
+ CORS headers are handled automatically by `Belt::LambdaHandler`. Controllers
144
+ don't need to set them manually. Configure allowed origins via environment
145
+ variables:
146
+
147
+ - `CORS_ALLOWED_ORIGINS` — comma-separated origins
148
+ - `CUSTOMER_APP_DOMAIN` — primary app domain
149
+ - `OPS_APP_DOMAIN` — internal tools domain
150
+
151
+ ## See Also
152
+
153
+ - `belt explain routing` — how requests reach controllers
154
+ - `belt explain models` — ActiveItem ORM
155
+ - `belt explain parameters` — strong parameters in detail
@@ -0,0 +1,166 @@
1
+ # Deployment
2
+
3
+ Belt deploys serverless applications to AWS using Terraform. The CLI wraps
4
+ `terraform init/plan/apply` with conventions for environment management,
5
+ Lambda packaging, and pre-deploy backups.
6
+
7
+ ## Quick Deploy
8
+
9
+ ```bash
10
+ belt deploy <env> # init → plan → apply (interactive)
11
+ belt deploy prod --auto # skip confirmation prompt
12
+ belt deploy prod --skip-backup # skip pre-deploy backup
13
+ belt deploy --backup-only # create recovery point without deploying
14
+ ```
15
+
16
+ ## First-Time Setup
17
+
18
+ ```bash
19
+ # 1. Create S3 bucket for Terraform state
20
+ belt setup state
21
+
22
+ # 2. Scaffold an environment
23
+ belt generate environment dev01
24
+
25
+ # 3. Generate DynamoDB table definitions from schema
26
+ belt setup tables dev01
27
+
28
+ # 4. Initialize and deploy
29
+ belt deploy dev01
30
+ ```
31
+
32
+ ## Environment Structure
33
+
34
+ Each environment lives in `infrastructure/<env>/`:
35
+
36
+ ```
37
+ infrastructure/
38
+ ├── modules/
39
+ │ └── main/ # Shared Terraform module
40
+ ├── dev01/
41
+ │ ├── main.tf # Module reference + provider
42
+ │ ├── variables.tf # Variable declarations
43
+ │ ├── terraform.tfvars # Environment-specific values
44
+ │ ├── backend.tf # S3 state backend
45
+ │ └── outputs.tf # Exported values
46
+ └── prod/
47
+ └── ...
48
+ ```
49
+
50
+ ## What Gets Deployed
51
+
52
+ The Conveyor Belt Terraform provider reads your Ruby DSL and creates:
53
+
54
+ 1. **API Gateway** — HTTP API with routes matching your DSL
55
+ 2. **Lambda functions** — packaged Ruby code (one per `gateway`/`function` block)
56
+ 3. **IAM roles** — least-privilege policies for DynamoDB table access
57
+ 4. **CloudWatch logs** — log groups for each Lambda
58
+ 5. **DynamoDB tables** — from your schema definition
59
+ 6. **Custom domain** — if configured (Route53 + ACM certificate)
60
+
61
+ ## Lambda Packaging
62
+
63
+ Belt packages the `lambda/` directory plus vendored gems. For `path:` gems
64
+ (local development), Belt materializes them into `vendor/cache` automatically.
65
+
66
+ The Lambda entry point is specified in `config/lambda/<name>.yml`:
67
+
68
+ ```yaml
69
+ handler: lambda/<name>.lambda_handler
70
+ runtime: ruby3.3
71
+ timeout: 30
72
+ memory: 256
73
+ environment:
74
+ ENVIRONMENT: ${var.environment}
75
+ ```
76
+
77
+ ## Route Manifests
78
+
79
+ Before deploying, generate the route manifest used at runtime:
80
+
81
+ ```bash
82
+ belt routes --namespace api
83
+ # → writes lambda/lib/routes/api_routes.rb
84
+ ```
85
+
86
+ This is typically done automatically by `belt deploy`.
87
+
88
+ ## Sidecar Lambda Zips
89
+
90
+ Conveyor Belt packages Ruby lambdas. Some apps also have standalone
91
+ `aws_lambda_function` resources (Node image processors, Cognito triggers)
92
+ whose Terraform uses `filename` + `filebase64sha256` pointing at a zip on disk.
93
+
94
+ `belt deploy`, `belt plan`, and `belt apply` scan `infrastructure/**/*.tf` for
95
+ those zip paths and build any that are missing (or whose source hash changed):
96
+
97
+ - Directory with `package.json` — `npm ci` in Docker (`linux/amd64`, Lambda
98
+ Node image) so native addons like `sharp` match Lambda, then zip
99
+ - Plain JS directory — zip the `.js` / `.mjs` files
100
+
101
+ The zip lives next to the source (`image-processor/image-processor.zip`). This
102
+ is the same pre-terraform step Stowzilla's `scripts/deploy.sh` does for the
103
+ image processor. Existing zips with no hash file are left alone.
104
+
105
+ Docker must be running for Node packages.
106
+
107
+ ## Terraform Commands
108
+
109
+ Belt wraps Terraform with environment awareness:
110
+
111
+ ```bash
112
+ belt init <env> # terraform init with correct backend
113
+ belt plan <env> # terraform plan
114
+ belt apply <env> # terraform apply
115
+ belt destroy <env> # terraform destroy (careful!)
116
+ belt output <env> # terraform output
117
+ ```
118
+
119
+ Or use `belt deploy <env>` which runs init → plan → apply in sequence.
120
+
121
+ ## Pre-Deploy Backups
122
+
123
+ Configure in `infrastructure/<env>/belt.rb`:
124
+
125
+ ```ruby
126
+ Belt.configure do |config|
127
+ config.backups do
128
+ dynamodb :all
129
+ retention snapshots: 90
130
+ end
131
+ end
132
+ ```
133
+
134
+ Backups run automatically before each deploy (DynamoDB snapshots, Cognito
135
+ exports, S3 syncs). See `belt explain backups` for full documentation.
136
+
137
+ ## Environment Variables
138
+
139
+ Set `BELT_ENV` to avoid typing the environment every time:
140
+
141
+ ```bash
142
+ export BELT_ENV=dev01
143
+ belt deploy # uses BELT_ENV
144
+ belt deploy prod # explicit arg wins
145
+ ```
146
+
147
+ ## Frontend Deployment
148
+
149
+ For apps with a frontend:
150
+
151
+ ```bash
152
+ belt deploy frontend <env> # build + deploy all frontends
153
+ belt deploy frontend <env> --frontend ops # deploy one named frontend
154
+ belt frontend env <env> # generate .env from Terraform outputs
155
+ belt frontend list # show configured frontends
156
+ ```
157
+
158
+ The directory is `frontend/` by default. Multiple SPAs are declared in
159
+ `config/frontends.yml` — see `belt explain frontend`. Full `belt deploy <env>`
160
+ deploys every configured frontend after terraform apply.
161
+
162
+ ## See Also
163
+
164
+ - `belt explain routing` — how routes map to infrastructure
165
+ - `belt explain backups` — pre-deploy backup configuration
166
+ - `belt doctor` — check system dependencies before deploying
@@ -0,0 +1,103 @@
1
+ # Frontends
2
+
3
+ Belt can host one or more JavaScript SPAs (React, Vue, or Svelte) next to the
4
+ Lambda API. A typical app has a single `frontend/` directory. Some apps — Stowzilla
5
+ is the example that drove this — have several independent SPAs (customer, ops,
6
+ partners) that share the same models and API.
7
+
8
+ ## Single frontend (default)
9
+
10
+ ```bash
11
+ belt new blog --frontend react
12
+ # or, in an existing app:
13
+ belt generate frontend react
14
+ ```
15
+
16
+ That creates `frontend/`, S3 + CloudFront infrastructure, and wires CORS so the
17
+ SPA can call the API. No config file is required.
18
+
19
+ ```bash
20
+ belt server # local Vite dev server
21
+ belt frontend env dev01 # write frontend/.env from terraform outputs
22
+ belt deploy frontend dev01 # npm ci → build → S3 sync → CloudFront invalidation
23
+ ```
24
+
25
+ ## Multiple frontends
26
+
27
+ Declare them in `config/frontends.yml` (or `.belt/frontends.yml`):
28
+
29
+ ```yaml
30
+ frontends:
31
+ customer:
32
+ path: app
33
+ dist: build
34
+ default: true
35
+ bucket_output: web_app_bucket_name
36
+ url_output: web_app_url
37
+ cloudfront_domain_output: web_app_cloudfront_domain
38
+ ops:
39
+ path: ops-app
40
+ dist: build
41
+ bucket_output: ops_app_bucket_name
42
+ url_output: ops_app_url
43
+ cloudfront_domain_output: ops_app_cloudfront_domain
44
+ ```
45
+
46
+ `path` is the directory. `dist` is the build output (`dist` by default; belt
47
+ also auto-detects `build/` after `npm run build`). Terraform output names default
48
+ to `frontend_*` for the `frontend` app and `{name}_frontend_*` for others — override
49
+ them when existing infrastructure uses different names.
50
+
51
+ If terraform exports a CloudFront **domain** instead of a distribution ID, set
52
+ `cloudfront_domain_output` and skip `distribution_output`. Belt looks the ID up
53
+ via AWS and will not probe `{name}_frontend_distribution_id`.
54
+
55
+ ```bash
56
+ belt frontend list
57
+ belt generate frontend react --name ops --path ops-app
58
+ belt generate views bag --frontend ops
59
+ belt generate scaffold order --frontend customer
60
+ belt server --frontend ops
61
+ belt frontend env dev01 --frontend customer
62
+ belt deploy frontend dev01 # all configured frontends
63
+ belt deploy frontend dev01 --frontend ops # just ops
64
+ ```
65
+
66
+ If several frontends exist and you omit `--frontend`, generators and `belt server`
67
+ use the one marked `default: true`. If none is default, they ask you to pick.
68
+
69
+ `belt deploy frontend <env>` with no `--frontend` deploys every frontend that
70
+ has a `package.json`. Full `belt deploy <env>` does the same after terraform apply.
71
+
72
+ ## Env maps
73
+
74
+ Each frontend can have its own `env.yml` mapping process env names to terraform
75
+ outputs:
76
+
77
+ ```yaml
78
+ # app/env.yml
79
+ VITE_API_URL: api_url
80
+ VITE_COGNITO_USER_POOL_ID: cognito_user_pool_id
81
+ VITE_COGNITO_CLIENT_ID: cognito_client_id
82
+ ```
83
+
84
+ The default `frontend/` directory also accepts `.belt/frontend_env.yml` as a
85
+ fallback. See the existing env-map behavior: only mapped keys are written into
86
+ `.env`; missing terraform outputs warn and do not clobber local values.
87
+
88
+ ## Infrastructure
89
+
90
+ `belt setup frontend` (and `belt generate frontend`) writes S3 + CloudFront into
91
+ `infrastructure/modules/app/frontend.tf`. Additional named frontends get
92
+ `{name}_frontend.tf` with unique resource names and outputs
93
+ (`ops_frontend_bucket_name`, …). Extra frontends get a CloudFront URL only —
94
+ custom DNS stays on the default frontend unless you add records yourself.
95
+
96
+ CORS: each CloudFront domain is added to `frontend_urls` on the conveyor-belt
97
+ resource so SPA → API calls work.
98
+
99
+ ## See Also
100
+
101
+ - `belt explain deployment` — how frontend deploy fits into `belt deploy`
102
+ - `belt explain generators` — `belt generate frontend` / views
103
+ - `belt explain structure` — where frontend directories live