@stacksjs/defaults 0.74.3 → 0.74.5

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 (30) hide show
  1. package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
  2. package/ai/skills/stacks-buddy/SKILL.md +53 -3
  3. package/ai/skills/stacks-cloud/SKILL.md +83 -11
  4. package/ai/skills/stacks-commerce/SKILL.md +1 -1
  5. package/ai/skills/stacks-composables/SKILL.md +1 -1
  6. package/ai/skills/stacks-dashboard/SKILL.md +2 -2
  7. package/ai/skills/stacks-deploy/SKILL.md +97 -24
  8. package/ai/skills/stacks-orm/SKILL.md +1 -1
  9. package/ai/skills/stacks-types/SKILL.md +1 -1
  10. package/ai/skills/stacks-writing-for-agents/SKILL.md +1 -1
  11. package/app/Actions/Dashboard/Analytics/WebAnalyticsAction.ts +2 -7
  12. package/app/Actions/Dashboard/Analytics/web-analytics-provider.ts +103 -0
  13. package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +24 -84
  14. package/app/Actions/Dashboard/Infrastructure/log-provider.ts +189 -0
  15. package/app/Actions/Dashboard/dashboard-provider.ts +170 -0
  16. package/app/Actions/Monitoring/ErrorGroupAction.ts +2 -4
  17. package/app/Actions/Monitoring/ErrorIndexAction.ts +2 -4
  18. package/app/Actions/Monitoring/ErrorShowAction.ts +4 -4
  19. package/app/Actions/Monitoring/ErrorStatsAction.ts +2 -4
  20. package/app/Actions/Monitoring/ErrorTimelineAction.ts +2 -4
  21. package/app/Actions/Monitoring/error-provider.ts +130 -0
  22. package/app/Models/EmailIdempotency.ts +4 -1
  23. package/app/Models/EmailSuppression.ts +4 -1
  24. package/app/Models/EmailWebhookEvent.ts +4 -1
  25. package/app/Models/Request.ts +4 -1
  26. package/functions/api-url.test.ts +61 -0
  27. package/functions/api-url.ts +19 -2
  28. package/ide/vscode/package.json +1 -1
  29. package/package.json +2 -2
  30. package/app/Actions/Buddy/CommandsAction.ts +0 -820
@@ -66,7 +66,7 @@ globalThis.toggleDark = toggleDark
66
66
  - **Custom Functions**: From `resources/functions/` (counter, dark mode, GPX, geo utilities)
67
67
 
68
68
  ### Server Auto-Imports (100+)
69
- - **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (60+ models)
69
+ - **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (97 models)
70
70
  - **Request Models**: UserRequest, PostRequest, OrderRequest, etc.
71
71
  - **Actions**: Action types and helpers
72
72
  - **Schema**: validation schema builder
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-buddy
3
- description: Use when working with the Stacks CLI (buddy/bud/stacks/stx) - understanding all 50+ commands with their flags and options, adding custom commands, the make:* scaffolding commands, development server commands, build commands, deployment commands, email/mail commands, environment management, or domain/DNS commands. Covers @stacksjs/buddy and all CLI command files.
3
+ description: Use when working with the Stacks CLI (buddy/bud/stacks/stx) - understanding every command with its flags and options, adding custom commands, the make:* scaffolding commands, development server commands, build commands, deployment commands, email/mail commands, environment management, or domain/DNS commands. Covers @stacksjs/buddy and all CLI command files.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -8,7 +8,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
8
8
 
9
9
  # Stacks Buddy CLI
10
10
 
11
- The complete CLI runtime for the Stacks framework with 50+ commands, lazy-loaded for fast cold starts.
11
+ The complete CLI runtime for the Stacks framework, lazy-loaded for fast cold starts. `docs/guide/buddy/commands.md` lists every command and is generated from the runtime registry, so it is the count as well as the reference.
12
12
 
13
13
  ## Key Paths
14
14
  - Core package: `storage/framework/core/buddy/src/`
@@ -367,7 +367,57 @@ buddy env:check # validate environment configuration
367
367
  ## Cloud & Deployment
368
368
 
369
369
  ### `buddy deploy` - Deploy to cloud
370
- Handles full deployment workflow: prerequisites check, pantry install, env setup, APP_KEY, AWS credentials (from .env.{env} or ~/.aws/credentials), domain setup, email DNS records (DKIM, MX, SPF, DMARC), mail user creation.
370
+ Handles full deployment workflow: prerequisites check, pantry install, env setup, APP_KEY, provider credentials, domain setup, email DNS records (DKIM, MX, SPF, DMARC), mail user creation.
371
+
372
+ ```bash
373
+ buddy deploy [env] # env is production | staging | development
374
+ --domain <domain> # override the domain this deploy publishes
375
+ -p/--project [project] # target a specific project
376
+ --prod # deploy to production
377
+ --staging # deploy to staging
378
+ --dev # deploy to development
379
+ --site <name> # deploy only this one site to the existing server
380
+ --docker # also build an OCI image with pantry and push it
381
+ --dry-run # preview the plan, change nothing
382
+ --yes # skip confirmation (required from a non-TTY)
383
+ -J/--json # machine-readable deployment preview
384
+ --verbose # verbose output
385
+
386
+ buddy deploy:rollback [site] # activate a preserved release (hetzner/ssh targets)
387
+ --env <environment> # environment to roll back (default: production)
388
+ --to <release> # preserved release id to activate
389
+ --dry-run # preview without changing the active release
390
+ --verbose # verbose output
391
+ ```
392
+
393
+ Where it deploys is `cloud.provider` in `config/cloud.ts`: `'aws'` (default, CloudFormation),
394
+ `'hetzner'` (provision a server, then deploy over SSH), or `'ssh'` (a host you already own).
395
+ `CLOUD_PROVIDER` overrides the config value.
396
+
397
+ ### `buddy server:*` - An SSH deploy target
398
+ For `cloud.provider: 'ssh'`, a Linux box you already own. The `raspberry-pi` profile tunes the
399
+ bootstrap for a small single-board computer.
400
+
401
+ ```bash
402
+ buddy server:flash # write an OS image to an SD card or USB disk
403
+ --os <name> # raspberry-pi-os-lite (default), raspberry-pi-os, ubuntu-24.04, ubuntu-26.04
404
+ --device <path> # the whole disk to write to, e.g. /dev/disk4
405
+ --list # list writable disks and exit
406
+ --dry-run # say what would happen without writing
407
+ --yes # skip the confirmation
408
+ buddy server:first-boot # write cloud-init user-data to the mounted boot partition
409
+ --hostname <name> # the board's hostname
410
+ --user <name> # the login user cloud-init creates
411
+ buddy server:doctor # preflight over SSH: arch, OS, memory, disk, sudo, clock, HTTPS
412
+ buddy server:setup # adopt and bootstrap the host (bun, rpx, systemd units)
413
+ buddy server:trust # install the box's local CA here, or emit a .mobileconfig
414
+ ```
415
+
416
+ Then `buddy deploy --prod` behaves exactly as it does for Hetzner. Connection details come from
417
+ `ssh.hosts` in `config/cloud.ts`, or from `TS_CLOUD_SSH_HOST`, `TS_CLOUD_SSH_USER`,
418
+ `TS_CLOUD_SSH_PORT` and `TS_CLOUD_SSH_KEY`, which win over the config. A host on a private address
419
+ publishes no DNS and requests no certificate; `TS_CLOUD_SSH_PUBLISH_DNS=1` forces publishing on
420
+ and `0` forces it off.
371
421
 
372
422
  ### `buddy cloud` - Cloud management
373
423
  ```bash
@@ -1,14 +1,29 @@
1
1
  ---
2
2
  name: stacks-cloud
3
- description: Use when deploying or managing cloud infrastructure for Stacks - AWS deployment via CloudFormation/CDK, server mode (EC2, ALB, VPC), serverless mode (Lambda, API Gateway, CloudFront), jump boxes, domain management (Route53), S3 storage, SES email, edge computing, security groups, IAM, or the cloud configuration. Covers @stacksjs/cloud, @stacksjs/deploy, storage/framework/cloud/, and cloud/.
3
+ description: Use when deploying or managing cloud infrastructure for Stacks - choosing between the AWS, Hetzner and SSH targets, AWS deployment via CloudFormation/CDK, server mode (EC2, ALB, VPC), serverless mode (Lambda, API Gateway, CloudFront), jump boxes, domain management (Route53), S3 storage, SES email, edge computing, security groups, IAM, the rpx gateway and systemd units on an SSH box, or the cloud configuration. Covers @stacksjs/cloud, @stacksjs/deploy, storage/framework/cloud/, and cloud/.
4
4
  license: MIT
5
- compatibility: Bun >= 1.3.0, TypeScript, AWS
5
+ compatibility: Bun >= 1.3.0, TypeScript, AWS / Hetzner / any 64-bit Debian or Ubuntu host over SSH
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
7
7
  ---
8
8
 
9
9
  # Stacks Cloud & Deployment
10
10
 
11
- AWS-focused cloud deployment using CloudFormation via `@stacksjs/ts-cloud`.
11
+ Cloud infrastructure for Stacks, across three deploy targets, all driven through
12
+ `@stacksjs/ts-cloud`.
13
+
14
+ ## Targets
15
+
16
+ `cloud.provider` in `config/cloud.ts` decides which one runs. `CLOUD_PROVIDER` in the environment
17
+ overrides it. With neither set, the provider is `aws`.
18
+
19
+ | `cloud.provider` | Infrastructure | Pipeline |
20
+ |---|---|---|
21
+ | `'aws'` (default) | CloudFormation stack: EC2/ALB/VPC or Lambda/API Gateway/CloudFront | Template generated and applied |
22
+ | `'hetzner'` | One Hetzner Cloud server, firewall, SSH key, managed services | Tarball over SSH, systemd units, rpx gateway |
23
+ | `'ssh'` | A host you already own; nothing is provisioned | Same tarball pipeline, after an in-place bootstrap |
24
+
25
+ Read `config/cloud.ts` before advising anything provider-specific. This repo uses `'hetzner'`.
26
+ Most of this page is the AWS target; the Hetzner and SSH sections are marked as such.
12
27
 
13
28
  ## Key Paths
14
29
  - Cloud package: `storage/framework/core/cloud/src/`
@@ -18,7 +33,7 @@ AWS-focused cloud deployment using CloudFormation via `@stacksjs/ts-cloud`.
18
33
  - Cloud driver state: `storage/cloud/` (ts-cloud's `stateDir`, set in `config/cloud.ts`)
19
34
  - Configuration: `config/cloud.ts`
20
35
 
21
- ## Deployment Modes
36
+ ## Deployment Modes (AWS)
22
37
 
23
38
  ### Server Mode (EC2)
24
39
  - EC2 instances with configurable types (t3.micro, t4g.nano, etc.)
@@ -37,7 +52,7 @@ AWS-focused cloud deployment using CloudFormation via `@stacksjs/ts-cloud`.
37
52
  - SQS queues for background jobs
38
53
  - DynamoDB (optional)
39
54
 
40
- ## Deployment Flow
55
+ ## Deployment Flow (AWS)
41
56
 
42
57
  ```bash
43
58
  buddy deploy # deploy to cloud
@@ -53,6 +68,51 @@ buddy deploy # deploy to cloud
53
68
  - Tags: Environment, Project, ManagedBy
54
69
  - OnFailure: ROLLBACK
55
70
 
71
+ ## Hetzner and SSH targets
72
+
73
+ Both deploy the same way once a host exists: build each site, package a release tarball, copy it
74
+ over SSH, install and restart systemd units, preserve the prior release, and write this project's
75
+ rpx gateway fragment.
76
+
77
+ ```typescript
78
+ // config/cloud.ts
79
+ cloud: { provider: 'hetzner' }, // HCLOUD_TOKEN or hetzner.apiToken
80
+ ```
81
+
82
+ ```typescript
83
+ cloud: { provider: 'ssh' },
84
+ ssh: {
85
+ profile: 'raspberry-pi', // or 'generic'
86
+ hosts: [{ host: 'pi-stacks.local', user: 'pi', port: 22, privateKeyPath: '~/.ssh/id_ed25519', role: 'app' }],
87
+ hostKey: 'pin', // 'pin' | 'accept-new' | 'insecure'
88
+ sudo: true,
89
+ publicIp: 'auto', // omit to stay LAN-only
90
+ lan: { hostname: 'pi-stacks.local', tls: 'local-ca' },
91
+ },
92
+ ```
93
+
94
+ Env beats config for the SSH target: `TS_CLOUD_SSH_HOST`, `TS_CLOUD_SSH_USER`,
95
+ `TS_CLOUD_SSH_PORT`, `TS_CLOUD_SSH_KEY`. The first host with no `role`, or `role: 'app'`, is the
96
+ one deployed to. Multi-host fleets are not supported.
97
+
98
+ **The LAN rule.** An SSH host on a private address publishes no DNS, requests no Let's Encrypt
99
+ certificate, skips the CDN and skips mail reconciliation, and prints the LAN URLs instead. Private
100
+ means RFC1918, `100.64/10` CGNAT, loopback, link-local, IPv6 unique-local or link-local, a
101
+ `.local` / `.internal` / `.lan` / `.intranet` / `.home.arpa` name, or a bare single-label
102
+ hostname. LAN HTTPS comes from a certificate authority on the box itself (rpx `localCa`), not from
103
+ Let's Encrypt. To publish: routable address, ports 80 and 443 forwarded, a site with a domain, and
104
+ `ssh.publicIp` set. `TS_CLOUD_SSH_PUBLISH_DNS=1` forces publishing on, `0` forces it off.
105
+
106
+ **Shared boxes.** Both targets support `cloud.attachTo: '<owner slug>'`. An attached project skips
107
+ provisioning and deploys only its own sites. Each project owns `/etc/rpx/sites.d/<slug>.json` and
108
+ replaces it wholesale, so `project.slug` must be unique on the box, and the config must declare
109
+ every domain that fragment currently serves or the deploy stops.
110
+
111
+ **State.** `storage/cloud/state/<stack>.json` records the host, SSH user and port, key path, host
112
+ key fingerprint, LAN address, profile and staging path. `buddy cloud` builds an `ssh` project's
113
+ fleet from the config plus those pins, since there is no provider API to enumerate; status reads
114
+ `unknown` because nothing polls the host. `buddy server:doctor` is what asks.
115
+
56
116
  ## Cloud Helper Functions
57
117
 
58
118
  ```typescript
@@ -136,7 +196,10 @@ export default {
136
196
 
137
197
  ```bash
138
198
  buddy deploy # deploy application
139
- buddy cloud --diff # show infrastructure changes
199
+ buddy deploy:rollback [site] # activate a preserved release (hetzner/ssh)
200
+ buddy server:doctor # preflight an ssh host
201
+ buddy server:setup # adopt and bootstrap an ssh host
202
+ buddy cloud --diff # show infrastructure changes (aws)
140
203
  buddy cloud --ssh # SSH into cloud
141
204
  buddy cloud --invalidate-cache # invalidate CDN
142
205
  buddy cloud:add --jump-box # add jump box instance
@@ -173,19 +236,28 @@ buddy domains:remove <domain> # remove domain
173
236
 
174
237
  ## Infrastructure Stack (storage/framework/cloud/)
175
238
 
176
- - `deploy.ts` — Main deployment script (CDK app entry)
177
- - `cdk.json` — CDK configuration with 54 AWS context settings
178
- - `package.json` — Cloud package dependencies
239
+ - `deploy.ts`: Main deployment script (CDK app entry)
240
+ - `cdk.json`: CDK configuration with 54 AWS context settings
241
+ - `package.json`: Cloud package dependencies
179
242
 
180
243
  Stack naming: `{slugified-app-name}-cloud`
181
244
 
182
245
  ## Gotchas
246
+ - Check `cloud.provider` first. AWS advice is wrong for a Hetzner or SSH box, and vice versa
247
+ - On Hetzner and SSH there is no CloudFormation, no CDK, no stack, and `buddy cloud --diff` has
248
+ nothing to diff
249
+ - An SSH host is adopted, never created. `buddy server:doctor` checks it and `buddy server:setup`
250
+ bootstraps it before the first deploy
251
+ - A key named in `ssh.hosts[].privateKeyPath` that is not on disk is caught up front now; it used
252
+ to surface much later as a bare `Permission denied (publickey)`
253
+ - A Raspberry Pi has no real-time clock, so apt and ACME can both fail in the first minute after
254
+ boot until NTP corrects it
183
255
  - AWS credentials MUST be configured (`buddy configure:aws` or env vars)
184
256
  - Default region is `us-east-1` (from AWS_DEFAULT_REGION env)
185
257
  - APP_KEY must be colon-separated format (validated during deployment)
186
258
  - Server mode uses EC2 + ALB; serverless uses Lambda + API Gateway + CloudFront
187
- - Jump boxes are optional — used for SSH access to private instances
188
- - `cloud:remove` with `--force` skips confirmation — destructive operation
259
+ - Jump boxes are optional, used for SSH access to private instances
260
+ - `cloud:remove` with `--force` skips confirmation and is destructive
189
261
  - CDK toolkit stack is named `stacks-toolkit`
190
262
  - Environment mapping: `local` → `development`, others preserved
191
263
  - Deploy hooks run before/after deployment for custom logic
@@ -8,7 +8,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
8
8
 
9
9
  # Stacks Commerce
10
10
 
11
- Comprehensive e-commerce module with 15 sub-modules and 20+ models.
11
+ Comprehensive e-commerce module with 15 sub-modules and 36 models.
12
12
 
13
13
  ## Key Paths
14
14
  - Core package: `storage/framework/core/commerce/src/`
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-composables
3
- description: Use when creating or using reactive composables in STX templates - 90+ composables for state management, DOM interaction, sensors, animation, browser APIs, async operations, or the complete list of auto-imported composables. Covers @stacksjs/composables.
3
+ description: Use when creating or using reactive composables in STX templates - 153 composables for state management, DOM interaction, sensors, animation, browser APIs, async operations, or the complete list of auto-imported composables. Covers @stacksjs/composables.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-dashboard
3
- description: Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the 250+ built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
3
+ description: Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the 399 built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -8,7 +8,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
8
8
 
9
9
  # Stacks Dashboard
10
10
 
11
- The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 250+ components, and a multi-section layout.
11
+ The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 399 components, and a multi-section layout.
12
12
 
13
13
  ## Key Paths
14
14
  - Dashboard components: `storage/framework/defaults/resources/components/Dashboard/`
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: stacks-deploy
3
- description: Use when deploying a Stacks application - the deployment workflow (build → deploy), pre/post deploy hooks, server vs serverless mode selection, first-time deployment setup, deployment troubleshooting, or the buddy deploy command. For cloud infrastructure details (EC2, Lambda, CloudFormation, Route53, IAM), see stacks-cloud.
3
+ description: Use when deploying a Stacks application - picking a deploy target (AWS, Hetzner, or a host you own over SSH), the deployment workflow (build → deploy), pre/post deploy hooks, server vs serverless mode selection, first-time deployment setup, rollback, deployment troubleshooting, or the buddy deploy command. For cloud infrastructure details (EC2, Lambda, CloudFormation, Route53, IAM, rpx, systemd), see stacks-cloud.
4
4
  license: MIT
5
- compatibility: Bun >= 1.3.0, TypeScript, AWS
5
+ compatibility: Bun >= 1.3.0, TypeScript, AWS / Hetzner / any 64-bit Debian or Ubuntu host over SSH
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
7
7
  ---
8
8
 
@@ -10,6 +10,23 @@ allowed-tools: Read Edit Write Bash Grep Glob
10
10
 
11
11
  The deployment workflow for Stacks applications.
12
12
 
13
+ ## Deploy Targets
14
+
15
+ `cloud.provider` in `config/cloud.ts` decides where `buddy deploy` ships. `CLOUD_PROVIDER` in the
16
+ environment overrides it. With neither set, the provider is `aws`.
17
+
18
+ | `cloud.provider` | Target | What the deploy does |
19
+ |---|---|---|
20
+ | `'aws'` (default) | AWS | Generates infrastructure, creates or updates a CloudFormation stack |
21
+ | `'hetzner'` | Hetzner Cloud | Provisions the server through the Hetzner API, then deploys over SSH |
22
+ | `'ssh'` | A host you already own | Adopts and bootstraps the host over SSH, then deploys to it |
23
+
24
+ Read the repo's own `config/cloud.ts` before assuming a target. This repo uses `'hetzner'`.
25
+
26
+ Hetzner and `ssh` share one pipeline: a release tarball copied over SSH, systemd units per site,
27
+ and the rpx gateway in front. They differ in whether a server is created for you. AWS is a
28
+ different pipeline entirely.
29
+
13
30
  ## Quick Deploy
14
31
 
15
32
  ```bash
@@ -20,36 +37,73 @@ Preview the complete environment-aware plan before changing anything:
20
37
 
21
38
  ```bash
22
39
  buddy deploy --dry-run
23
- buddy deploy --dry-run --env staging
40
+ buddy deploy staging --dry-run
24
41
  buddy deploy --dry-run --site docs
25
42
  buddy deploy --dry-run --json
26
43
  ```
27
44
 
28
- The preview uses the same environment transformation and ts-cloud site
29
- classification as a real deploy. It reports the ordered validation,
30
- infrastructure, build, package, release, runtime, gateway, DNS, TLS, and
31
- optional container operations. It exits before prerequisite setup, builds,
32
- packaging, hooks, provider calls, persistence, DNS, TLS, or service restarts.
33
- The JSON form emits a versioned `STACKS_DEPLOY_PREVIEW_JSON=` line for API and
34
- tool consumers.
45
+ The preview uses the same environment transformation and ts-cloud site classification as a real
46
+ deploy. It reports the ordered validation, infrastructure, build, package, release, runtime,
47
+ gateway, DNS, TLS, and optional container operations. It exits before prerequisite setup, builds,
48
+ packaging, hooks, provider calls, persistence, DNS, TLS, or service restarts. The JSON form emits
49
+ a versioned `STACKS_DEPLOY_PREVIEW_JSON=` line for API and tool consumers.
35
50
 
36
51
  ## Deployment Prerequisites
37
52
 
53
+ **AWS** (`provider: 'aws'`)
54
+
38
55
  1. **AWS credentials configured**: `buddy configure:aws`
39
56
  2. **APP_KEY generated**: `buddy key:generate` (must be colon-separated format)
40
57
  3. **APP_URL set** in `.env`
41
58
  4. **Team configured** in `config/team.ts`
42
59
 
60
+ **Hetzner** (`provider: 'hetzner'`)
61
+
62
+ 1. `HCLOUD_TOKEN` in the environment, or `hetzner.apiToken` in `config/cloud.ts`
63
+ 2. `~/.ssh/id_ed25519.pub` on the machine deploying: the driver registers it on the server
64
+
65
+ **SSH** (`provider: 'ssh'`)
66
+
67
+ 1. A host in `ssh.hosts`, or `TS_CLOUD_SSH_HOST` in the environment
68
+ 2. A key that reaches it, and passwordless sudo when the user is not root
69
+ 3. `buddy server:doctor` clean
70
+
43
71
  ## Deployment Flow
44
72
 
45
- 1. **Validation** — checks APP_KEY format, AWS region, app URL, team config
46
- 2. **Build** — compiles application for production
47
- 3. **Infrastructure** — generates CloudFormation template via ts-cloud
48
- 4. **Deploy** — creates or updates the CloudFormation stack
73
+ **AWS**
74
+
75
+ 1. **Validation**: checks APP_KEY format, AWS region, app URL, team config
76
+ 2. **Build**: compiles application for production
77
+ 3. **Infrastructure**: generates CloudFormation template via ts-cloud
78
+ 4. **Deploy**: creates or updates the CloudFormation stack
49
79
  - Capabilities: CAPABILITY_IAM, CAPABILITY_NAMED_IAM
50
80
  - OnFailure: ROLLBACK
51
81
  - Tags: Environment, Project, ManagedBy
52
82
 
83
+ **Hetzner and SSH**
84
+
85
+ 1. **Validation**: same prerequisite checks, plus the ts-cloud persistent-state capability check
86
+ 2. **Infrastructure**: Hetzner creates or reuses the server; SSH checks and bootstraps the host
87
+ 3. **Build and package**: builds each site, packages a release tarball
88
+ 4. **Release**: copies over SSH, installs systemd units, restarts, preserves the prior release
89
+ 5. **Gateway**: writes `/etc/rpx/sites.d/<slug>.json`
90
+ 6. **DNS and TLS**: reconciles A records and issues certificates, when publishing is allowed
91
+
92
+ ## The LAN rule (`provider: 'ssh'` only)
93
+
94
+ An SSH host on a private address publishes no DNS, requests no Let's Encrypt certificate, skips
95
+ the CDN and skips mail reconciliation. It prints the LAN URLs instead.
96
+
97
+ Private means RFC1918, `100.64/10` CGNAT, loopback, link-local, IPv6 unique-local or link-local,
98
+ a `.local` / `.internal` / `.lan` / `.intranet` / `.home.arpa` name, or a bare single-label
99
+ hostname. Publishing an A record for `192.168.1.42` would point every visitor's browser at
100
+ whatever occupies that address on their own network, and an ACME challenge cannot reach a host
101
+ the internet cannot route to.
102
+
103
+ To publish: give the host a routable address, forward 80 and 443 to it, declare a domain on a
104
+ site, and set `ssh.publicIp` (`'auto'` discovers it at deploy time). `TS_CLOUD_SSH_PUBLISH_DNS=1`
105
+ forces publishing on for a host behind a port forward the deploy cannot see; `0` forces it off.
106
+
53
107
  ## Deploy Hooks (cloud/deploy-script.ts)
54
108
 
55
109
  ```typescript
@@ -70,6 +124,8 @@ export default {
70
124
 
71
125
  ## Deployment Modes
72
126
 
127
+ Modes apply to the AWS target.
128
+
73
129
  ### Server Mode (default)
74
130
  - EC2 instances with ALB
75
131
  - Best for: full-stack apps, WebSocket support, persistent connections
@@ -83,8 +139,10 @@ export default {
83
139
  ## First Deployment Checklist
84
140
 
85
141
  ```bash
86
- # 1. Configure AWS
87
- buddy configure:aws
142
+ # 1. Configure the provider
143
+ buddy configure:aws # aws
144
+ # hetzner: set HCLOUD_TOKEN
145
+ # ssh: buddy server:doctor, then buddy server:setup
88
146
 
89
147
  # 2. Generate app key
90
148
  buddy key:generate
@@ -96,25 +154,40 @@ buddy env:set APP_ENV production
96
154
  # 4. Review cloud config
97
155
  # Edit config/cloud.ts
98
156
 
99
- # 5. Deploy
157
+ # 5. Preview, then deploy
158
+ buddy deploy --dry-run
100
159
  buddy deploy
101
160
  ```
102
161
 
103
162
  ## CLI Commands
104
163
 
105
164
  ```bash
106
- buddy deploy # deploy to cloud
165
+ buddy deploy [env] # deploy; env is production | staging | development
166
+ buddy deploy --prod # same, by flag (--staging, --dev)
107
167
  buddy deploy --dry-run # preview without changing anything
168
+ buddy deploy --site <name> # deploy one site to the existing server
169
+ buddy deploy --domain <d> # override the domain this deploy publishes
170
+ buddy deploy --yes # skip confirmation (required in CI)
171
+ buddy deploy:rollback [site] # activate a preserved release (--env, --to, --dry-run)
172
+ buddy server:doctor # preflight an ssh host
173
+ buddy server:setup # adopt and bootstrap an ssh host
108
174
  buddy cloud --diff # preview infrastructure changes before deploying
109
175
  buddy cloud --ssh # SSH into deployed server
110
- buddy cloud:remove # tear down infrastructure
176
+ buddy cloud:remove # tear down infrastructure (alias: undeploy)
111
177
  buddy cloud:cleanup # clean retained resources
112
178
  ```
113
179
 
114
180
  ## Gotchas
115
- - First deployment creates the CloudFormation stack — subsequent deploys update it
181
+ - Check `cloud.provider` before advising anything provider-specific. Half the AWS advice is wrong
182
+ for a Hetzner or SSH box, and vice versa.
183
+ - `buddy deploy --prod` from a non-interactive shell refuses without `--yes`, rather than hanging
184
+ on the confirmation prompt.
116
185
  - APP_KEY must be colon-separated format (validated during deployment)
117
- - Default region is `us-east-1` (from AWS_DEFAULT_REGION env)
118
- - `buddy cloud --diff` shows changes BEFORE deploying — always review first
119
- - Deploy hooks run in the deployment process, not on the target server
120
- - For infrastructure details (EC2, Lambda, VPC, etc.), see the `stacks-cloud` skill
186
+ - `buddy cloud --diff` shows changes BEFORE deploying, and only for AWS
187
+ - Deploy hooks run in the deployment process on your machine, not on the target server
188
+ - On Hetzner and SSH, a project's `/etc/rpx/sites.d/<slug>.json` is replaced wholesale, so
189
+ `project.slug` must be unique on a shared box and the config must declare every domain that
190
+ fragment serves
191
+ - `.env.<environment>` is decrypted at deploy time and shipped as every site's `.env`, so declare
192
+ `tenants` in `config/cloud.ts` on a shared box to strip other tenants' keys
193
+ - For infrastructure details, see the `stacks-cloud` skill
@@ -11,7 +11,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
11
11
  ## Key Paths
12
12
  - Core ORM package: `storage/framework/core/orm/src/`
13
13
  - ORM implementation: `storage/framework/orm/`
14
- - Model definitions: `storage/framework/defaults/app/Models/` (50+ models)
14
+ - Model definitions: `storage/framework/defaults/app/Models/` (97 models)
15
15
  - Application models: `app/Models/`
16
16
  - Default model templates: `storage/framework/defaults/app/Models/`
17
17
  - ORM type globals: `storage/framework/types/orm-globals.d.ts`
@@ -86,7 +86,7 @@ All model CRUD events: `model:created`, `model:updated`, `model:deleted`
86
86
 
87
87
  - **Content**: author, post, page
88
88
  - **Core**: user, activity, campaign, comment, email-list, notification, social-post, subscription, tag
89
- - **Commerce (30+ models)**: cart, cart-item, category, coupon, customer, order, order-item, payment, product, product-variant, review, shipping-method, shipping-rate, tax-rate, transaction, gift-card, license-key, and more
89
+ - **Commerce (36 models)**: cart, cart-item, category, coupon, customer, order, order-item, payment, product, product-variant, review, shipping-method, shipping-rate, tax-rate, transaction, gift-card, license-key, and more
90
90
 
91
91
  All payloads are `Record<string, any>`.
92
92
 
@@ -13,7 +13,7 @@ project `AGENTS.md`, a doc reached by a pointer. The packaging differs, the
13
13
  writing does not. The same levers make each one predictable, because the agent
14
14
  takes the same *process* every run rather than producing the same output.
15
15
 
16
- Stacks ships 100+ skills and expects projects to add their own under
16
+ Stacks ships 115 skills and expects projects to add their own under
17
17
  `app/Skills/`, so this is the skill that keeps that set from turning to sludge.
18
18
  The Stacks-specific mechanics (frontmatter, invocation, the override model,
19
19
  what `buddy setup:ai` does with the result) are in
@@ -1,14 +1,12 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { Request } from '@stacksjs/orm'
4
3
  import { response } from '@stacksjs/router'
5
4
  import { dashboardOperationalError } from '../dashboard-response'
6
5
  import {
7
- buildWebAnalytics,
8
6
  normalizeAnalyticsRange,
9
7
  normalizeAnalyticsScope,
10
- requestAnalyticsRow,
11
8
  } from './request-analytics'
9
+ import { readDashboardWebAnalytics } from './web-analytics-provider'
12
10
 
13
11
  export default new Action({
14
12
  name: 'WebAnalyticsAction',
@@ -29,10 +27,7 @@ export default new Action({
29
27
  }, 422)
30
28
  }
31
29
  try {
32
- const records = await Request.orderByDesc('id').limit(10_000).get()
33
- const rows = records.map(requestAnalyticsRow)
34
-
35
- return buildWebAnalytics(rows, range, new Date(), scope)
30
+ return await readDashboardWebAnalytics({ range, scope })
36
31
  }
37
32
  catch (error) {
38
33
  return dashboardOperationalError(error, 'Web analytics records could not be read.', 'WebAnalyticsAction')
@@ -0,0 +1,103 @@
1
+ import type { DashboardProviderUnavailable } from '../dashboard-provider'
2
+ import type { AnalyticsRange, AnalyticsScope } from './request-analytics'
3
+ import { Request } from '@stacksjs/orm'
4
+ import { HQ_READ_UNAVAILABLE, readThroughProvider, resolveDashboardDriver } from '../dashboard-provider'
5
+ import { buildWebAnalytics, requestAnalyticsRow } from './request-analytics'
6
+
7
+ /**
8
+ * Where the web Analytics section reads from.
9
+ *
10
+ * The local provider aggregates this application's own recorded requests, and
11
+ * it keeps the read exactly as `WebAnalyticsAction` performed it, including the
12
+ * ten thousand row ceiling. That ceiling is applied by id before the range
13
+ * filter runs, so a busy application reports on its ten thousand most recent
14
+ * requests rather than on the window that was asked for. That is existing
15
+ * behaviour, it is load bearing for the current numbers, and changing it here
16
+ * would make this refactor impossible to verify as behaviour preserving. It is
17
+ * worth fixing on its own, against its own tests.
18
+ *
19
+ * Only web analytics is seamed. Sales and marketing analytics aggregate orders
20
+ * and campaigns from this application's own tables, which no hosted analytics
21
+ * product has any view of.
22
+ */
23
+
24
+ const ROW_CEILING = 10_000
25
+
26
+ /**
27
+ * The exact shape `buildWebAnalytics` returns, taken from the function itself.
28
+ *
29
+ * Deliberately derived rather than declared. Writing this shape out by hand and
30
+ * annotating the builder with it would narrow `source` from the literal
31
+ * `'requests'` to `string` and would re-type the two empty arrays it returns,
32
+ * which is a visible type change in a refactor that is supposed to have none.
33
+ */
34
+ export type DashboardWebAnalyticsPayload
35
+ = ReturnType<typeof buildWebAnalytics> & Partial<DashboardProviderUnavailable>
36
+
37
+ export interface DashboardWebAnalyticsQuery {
38
+ range: AnalyticsRange
39
+ scope: AnalyticsScope
40
+ }
41
+
42
+ export interface DashboardWebAnalyticsProvider {
43
+ webAnalytics: (query: DashboardWebAnalyticsQuery) => Promise<DashboardWebAnalyticsPayload>
44
+ }
45
+
46
+ /**
47
+ * An empty analytics payload, carrying the reason there is nothing to show.
48
+ *
49
+ * Built by running the real aggregation over no rows, so every key, every
50
+ * zero and every placeholder string is whatever the section already renders
51
+ * for an application that has served no traffic. A hand written empty object
52
+ * would drift from that the first time the builder gains a field.
53
+ */
54
+ export function emptyWebAnalyticsPayload(
55
+ reason: string,
56
+ query: DashboardWebAnalyticsQuery,
57
+ ): DashboardWebAnalyticsPayload {
58
+ return {
59
+ ...buildWebAnalytics([], query.range, new Date(), query.scope),
60
+ unavailable: reason,
61
+ }
62
+ }
63
+
64
+ function isWebAnalyticsPayload(payload: unknown): boolean {
65
+ if (!payload || typeof payload !== 'object')
66
+ return false
67
+ const candidate = payload as Partial<DashboardWebAnalyticsPayload>
68
+ return Boolean(candidate.overview)
69
+ && Array.isArray(candidate.traffic)
70
+ && Array.isArray(candidate.pages)
71
+ }
72
+
73
+ /**
74
+ * This application's own recorded requests.
75
+ *
76
+ * Not wrapped in a catch, so a local read failure still reaches the action's
77
+ * operational error exactly as it did before the seam existed.
78
+ */
79
+ export const localWebAnalyticsProvider: DashboardWebAnalyticsProvider = {
80
+ async webAnalytics(query: DashboardWebAnalyticsQuery): Promise<DashboardWebAnalyticsPayload> {
81
+ const records = await Request.orderByDesc('id').limit(ROW_CEILING).get()
82
+ const rows = records.map(requestAnalyticsRow)
83
+
84
+ return buildWebAnalytics(rows, query.range, new Date(), query.scope)
85
+ },
86
+ }
87
+
88
+ /** Reads the web Analytics section through whichever provider config selects. */
89
+ export async function readDashboardWebAnalytics(
90
+ query: DashboardWebAnalyticsQuery,
91
+ ): Promise<DashboardWebAnalyticsPayload> {
92
+ const driver = await resolveDashboardDriver('analytics')
93
+
94
+ if (driver.name === 'local')
95
+ return localWebAnalyticsProvider.webAnalytics(query)
96
+
97
+ return readThroughProvider(
98
+ 'analytics',
99
+ reason => emptyWebAnalyticsPayload(reason, query),
100
+ async () => emptyWebAnalyticsPayload(HQ_READ_UNAVAILABLE, query),
101
+ isWebAnalyticsPayload,
102
+ )
103
+ }