@mediamonks/canopy-cli 0.7.12 → 0.7.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.
- package/dist/{db-8Etjmzxh.mjs → db-DKnJQPm5.mjs} +2 -2
- package/dist/{deploy-B1WrPq5W.mjs → deploy-CnxJ4TE6.mjs} +2 -2
- package/dist/dev-BqLvttKc.mjs +2 -0
- package/dist/{dev-BnUYGQQm.mjs → dev-CyJT6EfQ.mjs} +4 -4
- package/dist/docs/ci.md +50 -0
- package/dist/docs/cli-create.md +46 -0
- package/dist/docs/cli-db.md +86 -0
- package/dist/docs/cli-delete.md +44 -0
- package/dist/docs/cli-deploy.md +58 -0
- package/dist/docs/cli-dev.md +82 -0
- package/dist/docs/cli-docs.md +47 -0
- package/dist/docs/cli-download.md +42 -0
- package/dist/docs/cli-egress.md +37 -0
- package/dist/docs/cli-feedback.md +85 -0
- package/dist/docs/cli-index.md +61 -0
- package/dist/docs/cli-init.md +46 -0
- package/dist/docs/cli-login.md +43 -0
- package/dist/docs/cli-logout.md +32 -0
- package/dist/docs/cli-logs.md +57 -0
- package/dist/docs/cli-open.md +41 -0
- package/dist/docs/cli-projects.md +43 -0
- package/dist/docs/cli-promote.md +44 -0
- package/dist/docs/cli-rollback.md +45 -0
- package/dist/docs/cli-skill.md +39 -0
- package/dist/docs/cli-staging.md +54 -0
- package/dist/docs/cli-token.md +47 -0
- package/dist/docs/cli-upgrade.md +43 -0
- package/dist/docs/cli-usage.md +43 -0
- package/dist/docs/cli-whoami.md +46 -0
- package/dist/docs/cli.md +86 -0
- package/dist/docs/data.md +85 -0
- package/dist/docs/deploy.md +81 -0
- package/dist/docs/dev.md +72 -0
- package/dist/docs/developers-agents-and-ai.md +95 -0
- package/dist/docs/developers-architecture.md +106 -0
- package/dist/docs/getting-started.md +108 -0
- package/dist/docs/index.json +336 -0
- package/dist/docs/infrastructure.md +406 -0
- package/dist/docs/migrations.md +183 -0
- package/dist/docs/overview-faq.md +105 -0
- package/dist/docs/overview-how-it-works.md +82 -0
- package/dist/docs/overview-use-cases.md +76 -0
- package/dist/docs/overview-what-is-canopy.md +75 -0
- package/dist/docs/platform-ai-usage.md +65 -0
- package/dist/docs/platform-client-access.md +54 -0
- package/dist/docs/platform-deploys-and-rollbacks.md +93 -0
- package/dist/docs/platform-egress.md +75 -0
- package/dist/docs/platform-feedback.md +119 -0
- package/dist/docs/platform-google.md +213 -0
- package/dist/docs/platform-members-and-access.md +71 -0
- package/dist/docs/platform-project-data.md +76 -0
- package/dist/docs/platform-schedules.md +90 -0
- package/dist/docs/platform-sites-and-profiles.md +61 -0
- package/dist/docs/porting-nextjs.md +233 -0
- package/dist/docs/sdk-ai.md +103 -0
- package/dist/docs/sdk-auth.md +57 -0
- package/dist/docs/sdk-database.md +126 -0
- package/dist/docs/sdk-files.md +82 -0
- package/dist/docs/sdk-services.md +58 -0
- package/dist/docs/sdk.md +126 -0
- package/dist/docs/staging.md +106 -0
- package/dist/docs/static-serving.md +323 -0
- package/dist/docs/troubleshooting.md +102 -0
- package/dist/{docs-DxB7NhZs.mjs → docs-3pQHiuTn.mjs} +2 -2
- package/dist/{egress-CN_axsAN.mjs → egress-BFSwa8Uu.mjs} +1 -1
- package/dist/{feedback-B-GXUBPs.mjs → feedback-DReTDDzD.mjs} +26 -7
- package/dist/index.mjs +25 -24
- package/dist/{local-BGRd_FMZ.mjs → local-BQxsp-ZQ.mjs} +2 -2
- package/dist/{login-D7dI8D9N.mjs → login-BDTO3nVw.mjs} +1 -1
- package/dist/{logs-DNiZquki.mjs → logs-C53y2eAS.mjs} +1 -1
- package/dist/{migrations-C-5x6SYf.mjs → migrations-C6EcKD6a.mjs} +1 -1
- package/dist/{open-DRPp4QJg.mjs → open-C5x3tX6I.mjs} +1 -1
- package/dist/{package-CDLufecy.mjs → package-CuyFMXam.mjs} +1 -1
- package/dist/{projects-C6JQjVqg.mjs → projects-MMbv7MmK.mjs} +1 -1
- package/dist/schedule-Cq9K0rnE.mjs +106 -0
- package/dist/skill/canopy/SKILL.md +160 -0
- package/dist/{skill-BOWfqwr2.mjs → skill-M4RHvuUB.mjs} +2 -2
- package/dist/{staging-Bw1D_J8M.mjs → staging-D8G4amjO.mjs} +1 -1
- package/dist/templates/spa/.github/workflows/canopy-deploy.yml +34 -0
- package/dist/templates/spa/AGENTS.md +47 -0
- package/dist/templates/spa/README.md +14 -0
- package/dist/templates/spa/_gitignore +4 -0
- package/dist/templates/spa/canopy.json +5 -0
- package/dist/templates/spa/index.html +12 -0
- package/dist/templates/spa/package.json +21 -0
- package/dist/templates/spa/src/app.tsx +25 -0
- package/dist/templates/spa/src/canopy.ts +5 -0
- package/dist/templates/spa/src/main.tsx +10 -0
- package/dist/templates/spa/src/style.css +29 -0
- package/dist/templates/spa/tsconfig.json +16 -0
- package/dist/templates/spa/vite.config.ts +7 -0
- package/dist/templates/static/AGENTS.md +45 -0
- package/dist/templates/static/README.md +9 -0
- package/dist/templates/static/_gitignore +2 -0
- package/dist/templates/static/canopy.json +4 -0
- package/dist/templates/static/index.html +21 -0
- package/dist/templates/static/main.js +9 -0
- package/dist/templates/static/style.css +29 -0
- package/dist/templates/worker/.dev.vars.example +9 -0
- package/dist/templates/worker/.github/workflows/canopy-deploy.yml +34 -0
- package/dist/templates/worker/AGENTS.md +63 -0
- package/dist/templates/worker/README.md +17 -0
- package/dist/templates/worker/_gitignore +5 -0
- package/dist/templates/worker/canopy.json +6 -0
- package/dist/templates/worker/index.html +12 -0
- package/dist/templates/worker/package.json +22 -0
- package/dist/templates/worker/src/app.tsx +25 -0
- package/dist/templates/worker/src/canopy.ts +5 -0
- package/dist/templates/worker/src/main.tsx +10 -0
- package/dist/templates/worker/src/style.css +29 -0
- package/dist/templates/worker/tsconfig.json +16 -0
- package/dist/templates/worker/vite.config.ts +7 -0
- package/dist/templates/worker/worker/index.ts +52 -0
- package/dist/{token-D7zrIF8G.mjs → token-BUxgRJVA.mjs} +1 -1
- package/dist/{ui-2mrJcjYC.mjs → ui-B1Px5y7q.mjs} +17 -0
- package/dist/{upgrade-fhfrid_O.mjs → upgrade-C8Xox-P-.mjs} +2 -2
- package/dist/{usage-CX1Izj_l.mjs → usage-DjNngyvy.mjs} +1 -1
- package/package.json +1 -1
- package/dist/dev-CySBxW7I.mjs +0 -2
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Infrastructure as code
|
|
3
|
+
description: Terraform for the account-level Cloudflare pieces Canopy needs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Canopy's Cloudflare footprint is small on purpose: almost everything
|
|
7
|
+
per-project is provisioned at runtime by canopy-api (`apps/api/src/cf.ts`,
|
|
8
|
+
reached through the Cloudflare `PlatformAdapter` in
|
|
9
|
+
`apps/api/src/platform/cloudflare.ts`), so the only things worth managing
|
|
10
|
+
declaratively are a handful of platform-wide resources. Those live in
|
|
11
|
+
[`infra/`](../../infra) as OpenTofu; the rest is bootstrap scripts and this
|
|
12
|
+
runbook.
|
|
13
|
+
|
|
14
|
+
**Live platform (2026-08-01):** Monks Explore account
|
|
15
|
+
`1c826c7b063fd9ca5e1b1bdaabd05feb`, zone `canopy.website`. The steps below
|
|
16
|
+
are the verified path used for that move (personal account
|
|
17
|
+
`9513cbfa5b182546f737501b10e1ed85` / `thanarie.dev` → company). Re-run them
|
|
18
|
+
to stand the platform up in another fresh account, or to rebuild after a
|
|
19
|
+
wipe.
|
|
20
|
+
|
|
21
|
+
Scope of a fresh stand-up:
|
|
22
|
+
|
|
23
|
+
1. the **platform workers** — `canopy-api` and `canopy-gateway` — deployed and
|
|
24
|
+
healthy (these aren't optional; nothing works without them), and
|
|
25
|
+
2. the **four core projects** — `dashboard`, `www`, `docs`, `kitchen-sink` —
|
|
26
|
+
re-created and deployed _on_ the platform like any other project.
|
|
27
|
+
|
|
28
|
+
Everyone else's projects are **not** carried over — a new account starts with
|
|
29
|
+
an empty control-plane registry (see [Non-goals](#non-goals)).
|
|
30
|
+
|
|
31
|
+
See decision **D14** in [`02-decisions.md`](../02-decisions.md) for the why.
|
|
32
|
+
|
|
33
|
+
## What's code vs. what's clicked
|
|
34
|
+
|
|
35
|
+
**OpenTofu** (`infra/*.tf`): zone, proxied wildcard DNS record, control-plane
|
|
36
|
+
D1, R2 bucket, AI Gateway shell, optional ACM cert pack.
|
|
37
|
+
|
|
38
|
+
**wrangler** (`apps/{api,gateway}/wrangler.{main,staging}.jsonc`): the two
|
|
39
|
+
platform Worker scripts, their bindings, and the `*.<zone>/*` route — one
|
|
40
|
+
config file per Cloudflare site (same names as CLI profiles). Deployed with
|
|
41
|
+
`pnpm run deploy:main` / `deploy:staging`, not Tofu. Plain `wrangler.jsonc`
|
|
42
|
+
is local `wrangler dev` only.
|
|
43
|
+
|
|
44
|
+
**`canopy deploy`** (`canopy.json` in each project): the core projects
|
|
45
|
+
(`dashboard`, `www`, `docs`, `kitchen-sink`) — ordinary platform projects
|
|
46
|
+
that dogfood the product, not infrastructure.
|
|
47
|
+
|
|
48
|
+
**Bootstrap scripts** (`infra/scripts/`): runtime API tokens, worker secrets,
|
|
49
|
+
AI Gateway BYOK keys + spend rule.
|
|
50
|
+
|
|
51
|
+
**Manual / billing (clicked once in the dashboard):** create the account &
|
|
52
|
+
attach it to the org; add the domain; mint bootstrap API token(s); optionally
|
|
53
|
+
enable paid Workers products. Plus OneLogin: redirect URIs for the new host
|
|
54
|
+
(if they aren't already set).
|
|
55
|
+
|
|
56
|
+
## Non-goals
|
|
57
|
+
|
|
58
|
+
- **No project-data migration.** Per-project D1s, R2 blobs, and deploy history
|
|
59
|
+
stay in the old account. The new control-plane D1 is empty; the core
|
|
60
|
+
projects are re-created from scratch (they hold no precious data — `www`,
|
|
61
|
+
`dashboard`, and `docs` are stateless, `kitchen-sink` is a demo), and everyone else's
|
|
62
|
+
projects are re-created as needed. (If that ever changes, the export/import
|
|
63
|
+
primitives already exist in `apps/api/src/cf.ts` and `canopy db snapshot|restore`.)
|
|
64
|
+
- **No Tofu management of Workers/secrets/per-project resources** — see the
|
|
65
|
+
table in [`infra/README.md`](../../infra/README.md).
|
|
66
|
+
- **No per-project Workers on the Cloudflare target today.** The CF
|
|
67
|
+
`PlatformAdapter` reports `capabilities.workers = false` (and
|
|
68
|
+
schedules/secrets likewise). Project backends, schedules, and project
|
|
69
|
+
secrets live on targets that support them (e.g. GCP). Kitchen-sink's
|
|
70
|
+
`worker` field in `canopy.json` must be omitted for a CF deploy — see
|
|
71
|
+
[step 10](#10-log-in-grant-yourself-admin-then-deploy-the-core-projects).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Rollout — step by step
|
|
76
|
+
|
|
77
|
+
Prereqs on your machine: `tofu` (or `terraform`), `wrangler` (via `pnpm` in
|
|
78
|
+
the repo), `curl`, `jq`, `openssl`. Confirm wrangler can see the target
|
|
79
|
+
account after `wrangler login`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pnpm --filter api exec wrangler whoami
|
|
83
|
+
# expect the new account listed (e.g. "Monks Explore")
|
|
84
|
+
export CLOUDFLARE_ACCOUNT_ID="<account_id>" # required when you belong to >1 account
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 1. Account & billing (dashboard)
|
|
88
|
+
|
|
89
|
+
1. Create / choose the new account; attach it to the company org; ensure your
|
|
90
|
+
user is a member with rights to manage Workers / D1 / R2 / DNS.
|
|
91
|
+
2. Add `canopy.website` to the account and repoint its nameservers at
|
|
92
|
+
Cloudflare. Wait for the zone to go **Active**. Note the **Account ID**
|
|
93
|
+
(`tofu` var `account_id`) and the **Zone ID**.
|
|
94
|
+
3. **Delete any auto-created `www` record.** Cloudflare's onboarding scan often
|
|
95
|
+
adds a `www` A/CNAME. An explicit record beats the wildcard, and if it's
|
|
96
|
+
DNS-only (grey cloud) the request never reaches Workers — which would break
|
|
97
|
+
the `www` core project. The wildcard `*` alone must serve `www.canopy.website`.
|
|
98
|
+
(You can confirm via API once the bootstrap token exists — an empty zone is
|
|
99
|
+
fine.)
|
|
100
|
+
4. **R2** must work (creating a probe bucket is enough to confirm). **Workers
|
|
101
|
+
Paid + Workers for Platforms** are required for per-project workers
|
|
102
|
+
(Phase 2 item 1, D18): once the subscriptions are active, run
|
|
103
|
+
`infra/scripts/create-dispatch-namespace.sh` to create the
|
|
104
|
+
`canopy-projects` dispatch namespace (the `dispatch_namespaces` bindings in
|
|
105
|
+
`apps/api/wrangler.main.jsonc` and `apps/gateway/wrangler.main.jsonc` fail
|
|
106
|
+
to deploy without it), and make sure `CF_API_TOKEN` carries **Workers
|
|
107
|
+
Scripts Write** (re-mint with `create-api-tokens.sh` if it predates this).
|
|
108
|
+
A site without the binding still runs — workers/schedules/secrets simply
|
|
109
|
+
answer 501. **No Advanced Certificate Manager** — `*.canopy.website`
|
|
110
|
+
is a single-level wildcard covered by free Universal SSL. (ACM would only
|
|
111
|
+
be needed if nesting as `*.canopy.<some-other-domain>`.)
|
|
112
|
+
|
|
113
|
+
> **Apex is not covered.** `*` matches only one label, so `canopy.website`
|
|
114
|
+
> itself isn't served by the gateway (by design — see
|
|
115
|
+
> `apps/gateway/wrangler.main.jsonc`).
|
|
116
|
+
> Decide separately whether the apex should redirect to `www.canopy.website`
|
|
117
|
+
> (a Cloudflare Redirect Rule, cheap to add later); nothing in this runbook
|
|
118
|
+
> depends on it.
|
|
119
|
+
|
|
120
|
+
### 2. Bootstrap API token(s) (dashboard)
|
|
121
|
+
|
|
122
|
+
You need credentials that can (a) run OpenTofu + wrangler ops, and (b) mint
|
|
123
|
+
the least-privilege runtime tokens. In practice that is often **two**
|
|
124
|
+
account-owned tokens — a single token that mixes zone DNS with **Account API
|
|
125
|
+
Tokens · Write** has been flaky when edited in place.
|
|
126
|
+
|
|
127
|
+
**Token A — `canopy-bootstrap` (Tofu + deploy ops)**
|
|
128
|
+
|
|
129
|
+
| Resource | Permissions |
|
|
130
|
+
| ------------------------------------------------------------ | ----------------------------------------------------------------------- |
|
|
131
|
+
| Account | D1 Edit, Workers R2 Storage Edit, AI Gateway Edit, Workers Scripts Edit |
|
|
132
|
+
| Zone `canopy.website` (nested under the account — see below) | Zone Edit, DNS Edit, Workers Routes Edit |
|
|
133
|
+
|
|
134
|
+
**Token B — mint token (for `create-api-tokens.sh` only)**
|
|
135
|
+
|
|
136
|
+
| Resource | Permissions |
|
|
137
|
+
| -------- | ----------------------------------------- |
|
|
138
|
+
| Account | **Account API Tokens · Write** (and Read) |
|
|
139
|
+
|
|
140
|
+
**Pitfalls we hit:**
|
|
141
|
+
|
|
142
|
+
- The dashboard's "DNS" picks are easy to get wrong. You need zone-scoped
|
|
143
|
+
**DNS Write** / **Zone Write**, _not_ Account DNS Settings, DNS Views,
|
|
144
|
+
DNS Firewall, or Registrar Domains.
|
|
145
|
+
- For **account-owned** tokens, zone resources must be **nested under the
|
|
146
|
+
account** in the policy JSON shape
|
|
147
|
+
`com.cloudflare.api.account.<account_id> → com.cloudflare.api.account.zone.<zone_id>`.
|
|
148
|
+
A top-level zone resource can look correct in the UI and still 10000-auth on
|
|
149
|
+
`/zones/:id/dns_records`.
|
|
150
|
+
- `/user/tokens/verify` returns "Invalid API Token" for account-owned tokens —
|
|
151
|
+
use `GET /accounts/<account_id>/tokens/verify` instead.
|
|
152
|
+
- Wrangler **OAuth** can list/create D1 and R2 and deploy Workers, but it
|
|
153
|
+
cannot manage DNS (and often cannot call AI Gateway). Don't rely on OAuth
|
|
154
|
+
alone for step 3.
|
|
155
|
+
|
|
156
|
+
Store tokens outside git. Convenience pattern used in the move:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
cd infra
|
|
160
|
+
# gitignored via infra/.gitignore (.env / .env.*)
|
|
161
|
+
cat > .env <<'EOF'
|
|
162
|
+
CLOUDFLARE_API_TOKEN="<token A — canopy-bootstrap>"
|
|
163
|
+
CLOUDFLARE_TOKEN_MINT_TOKEN="<token B — API Tokens Write>"
|
|
164
|
+
EOF
|
|
165
|
+
set -a && source .env && set +a
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 3. Provision static infra (OpenTofu)
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
cd infra
|
|
172
|
+
cp terraform.tfvars.example terraform.tfvars # set account_id + zone_name
|
|
173
|
+
set -a && source .env && set +a # CLOUDFLARE_API_TOKEN = token A
|
|
174
|
+
tofu init
|
|
175
|
+
tofu import cloudflare_zone.platform <zone_id> # adopt the domain from step 1
|
|
176
|
+
tofu plan # review — should be creates only
|
|
177
|
+
tofu apply
|
|
178
|
+
tofu output wrangler_patch_hint
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Expect four creates: wildcard DNS `*`, D1 `canopy-control`, R2 `canopy-assets`,
|
|
182
|
+
AI Gateway `canopy`. (If the AI Gateway resource fails schema validation,
|
|
183
|
+
check `cache_invalidate_on_update` and friends against the pinned
|
|
184
|
+
`cloudflare` provider minor — it shifted in ~> 5.22.)
|
|
185
|
+
|
|
186
|
+
The R2 remote-state backend in `versions.tf` is commented out; for a solo
|
|
187
|
+
stand-up, local state is fine. If you want shared state, create a
|
|
188
|
+
`canopy-tfstate` bucket first, then uncomment and `tofu init -migrate-state`.
|
|
189
|
+
|
|
190
|
+
### 4. Point the apps at the new resources
|
|
191
|
+
|
|
192
|
+
Edit the **site** wrangler configs (not the local `wrangler.jsonc`) using
|
|
193
|
+
`tofu output wrangler_patch_hint`. For production that is
|
|
194
|
+
`wrangler.main.jsonc` in both apps; for the personal/staging account use
|
|
195
|
+
`wrangler.staging.jsonc`. Keep both files in git — never comment-flip a
|
|
196
|
+
single file to switch accounts.
|
|
197
|
+
|
|
198
|
+
**`apps/api/wrangler.<site>.jsonc`:**
|
|
199
|
+
|
|
200
|
+
- `account_id` / `vars.CF_ACCOUNT_ID` → account id
|
|
201
|
+
- `d1_databases[0].database_id` → control-plane D1 UUID
|
|
202
|
+
- `vars.AI_GATEWAY_ID` → unchanged (`canopy`) unless renamed
|
|
203
|
+
- `r2_buckets[0].bucket_name` → unchanged (`canopy-assets`) unless renamed
|
|
204
|
+
|
|
205
|
+
**`apps/gateway/wrangler.<site>.jsonc`:**
|
|
206
|
+
|
|
207
|
+
- `account_id` → same account id
|
|
208
|
+
- `routes[0]` → `{ "pattern": "*.canopy.website/*", "zone_name": "canopy.website" }`
|
|
209
|
+
(or `*.thanarie.dev/*` for staging)
|
|
210
|
+
- `vars.COOKIE_DOMAIN` / `AUTH_HOST` / `API_HOST` → that site's hosts
|
|
211
|
+
|
|
212
|
+
Also point the CLI at the site (`canopy-dev --profile main` / `staging`, or
|
|
213
|
+
`CANOPY_API_URL=https://api.<domain>`). Until the workers exist, skip the
|
|
214
|
+
CLI bits and come back in step 10.
|
|
215
|
+
|
|
216
|
+
### 5. Runtime tokens (script)
|
|
217
|
+
|
|
218
|
+
Mint with **token B** (API Tokens · Write). Token A cannot create other
|
|
219
|
+
tokens if it lacks that scope (and a token that _has_ Tokens·Write cannot
|
|
220
|
+
always be used as a "sub-token" parent for another Tokens·Write policy):
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
cd infra
|
|
224
|
+
set -a && source .env && set +a
|
|
225
|
+
export CLOUDFLARE_API_TOKEN="$CLOUDFLARE_TOKEN_MINT_TOKEN"
|
|
226
|
+
CANOPY_ACCOUNT_ID="$(tofu output -raw account_id)" \
|
|
227
|
+
./scripts/create-api-tokens.sh | tee .env.runtime
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Prints `CF_API_TOKEN` and `AI_GATEWAY_TOKEN` once. Keep them for secrets
|
|
231
|
+
(`.env.runtime` is gitignored via `.env.*`).
|
|
232
|
+
|
|
233
|
+
### 6. Apply the control-plane D1 schema
|
|
234
|
+
|
|
235
|
+
The Tofu-created D1 is empty. Load the schema into the remote (production) DB —
|
|
236
|
+
the `db:setup` script targets `--local`, so pass `--remote` explicitly:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
cd ../apps/api
|
|
240
|
+
# Use the site config that matches the account (main = canopy.website).
|
|
241
|
+
pnpm exec wrangler d1 execute canopy-control --remote --file schema.sql \
|
|
242
|
+
-c wrangler.main.jsonc
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Do **not** run `seed.local.sql` against remote — it's local dev seed data. Your
|
|
246
|
+
user row doesn't exist yet either; it's auto-provisioned on first login, and
|
|
247
|
+
admin is granted in step 10.
|
|
248
|
+
|
|
249
|
+
### 7. Deploy the platform workers
|
|
250
|
+
|
|
251
|
+
`wrangler secret put` needs the Worker script to exist, so deploy **before**
|
|
252
|
+
setting secrets. Use each package's `run deploy` script — bare
|
|
253
|
+
`pnpm --filter api deploy` is pnpm's own `deploy` command and will fail with
|
|
254
|
+
`ERR_PNPM_INVALID_DEPLOY_TARGET`:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# from repo root — pick the site (deploy aliases deploy:main)
|
|
258
|
+
pnpm --filter api run deploy:main # or deploy:staging
|
|
259
|
+
pnpm --filter gateway run deploy:main # same site
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Sanity check: `curl -I https://auth.canopy.website` should 302 toward
|
|
263
|
+
`/auth/login` (gateway alive). `curl -I https://auth.canopy.website/auth/login`
|
|
264
|
+
should 302 to OneLogin with
|
|
265
|
+
`redirect_uri=https://auth.canopy.website/auth/callback`.
|
|
266
|
+
|
|
267
|
+
### 8. Worker secrets (script)
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
cd infra
|
|
271
|
+
set -a && source .env.runtime && set +a # CF_API_TOKEN + AI_GATEWAY_TOKEN
|
|
272
|
+
# OIDC_CLIENT_SECRET: same OneLogin confidential-client secret as before if
|
|
273
|
+
# the app is unchanged. SESSION_SECRET: blank prompt = generate a fresh one.
|
|
274
|
+
OIDC_CLIENT_SECRET="…" CF_API_TOKEN="…" AI_GATEWAY_TOKEN="…" \
|
|
275
|
+
./scripts/bootstrap-secrets.sh main # or: staging
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Sets secrets on both workers; **`SESSION_SECRET` is written identically to api
|
|
279
|
+
and gateway** — the script handles that. Secrets take effect immediately (no
|
|
280
|
+
redeploy required).
|
|
281
|
+
|
|
282
|
+
### 9. OneLogin (external, dashboard)
|
|
283
|
+
|
|
284
|
+
Only if the host changed and the apps aren't already pointed at it. OneLogin
|
|
285
|
+
matches redirect URIs exactly (no wildcards):
|
|
286
|
+
|
|
287
|
+
- Gateway confidential client → `https://auth.canopy.website/auth/callback`
|
|
288
|
+
- CLI PKCE client → unchanged (`http://127.0.0.1:8976/callback`, local)
|
|
289
|
+
|
|
290
|
+
If you already configured these for `canopy.website` before the move, skip.
|
|
291
|
+
|
|
292
|
+
### 10. Log in, grant yourself admin, then deploy the core projects
|
|
293
|
+
|
|
294
|
+
The four core projects are ordinary platform projects that dogfood the product.
|
|
295
|
+
Three of their slugs (`dashboard`, `www`, `docs`) are in `RESERVED_SLUGS`
|
|
296
|
+
(`apps/api/src/routes/projects.ts`), so **only an admin can create them** —
|
|
297
|
+
hence the ordering here.
|
|
298
|
+
|
|
299
|
+
Point the local CLI at the new platform (and rebuild if you just changed
|
|
300
|
+
defaults):
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# canopy-dev = this repo's CLI (packages/cli → vp pack), not the npm `canopy`
|
|
304
|
+
export CANOPY_API_URL="https://canopy-api.monks.workers.dev"
|
|
305
|
+
export CANOPY_PROJECT_DOMAIN="canopy.website"
|
|
306
|
+
export CANOPY_PROJECT_PROTOCOL="https"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
1. `canopy-dev login` → browser PKCE against OneLogin; your user row is
|
|
310
|
+
auto-provisioned in the new control DB. Confirm with `canopy-dev whoami`
|
|
311
|
+
(apiUrl should be the new workers.dev host).
|
|
312
|
+
2. Grant admin (the row only exists after step 1). The first admin on a fresh
|
|
313
|
+
control DB still needs SQL; after that, admins can promote others from the
|
|
314
|
+
dashboard **Users** page:
|
|
315
|
+
```bash
|
|
316
|
+
pnpm --filter api exec wrangler d1 execute canopy-control --remote \
|
|
317
|
+
--command "UPDATE users SET is_admin = 1 WHERE email = 'you@monks.com'"
|
|
318
|
+
```
|
|
319
|
+
3. Register each project remotely, then deploy it. **`--no-scaffold` is
|
|
320
|
+
required** — `create` otherwise writes a starter template into `./<slug>/`
|
|
321
|
+
(and prompts for a template picker), which inside an existing app means a
|
|
322
|
+
junk `apps/dashboard/dashboard/` directory. `--no-scaffold` registers only,
|
|
323
|
+
no files written. The `--slug` must match the `slug` in that directory's
|
|
324
|
+
existing `canopy.json`, since that's what `deploy` publishes to:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# Always pass --profile <site> for the destination (main / staging / gcp-pilot).
|
|
328
|
+
# Examples below use main (production dogfood); swap the id when targeting another site.
|
|
329
|
+
cd apps/dashboard
|
|
330
|
+
canopy-dev --profile main create "Canopy Dashboard" --slug dashboard --no-scaffold
|
|
331
|
+
canopy-dev --profile main deploy
|
|
332
|
+
|
|
333
|
+
cd ../www
|
|
334
|
+
canopy-dev --profile main create "Canopy" --slug www --no-scaffold
|
|
335
|
+
canopy-dev --profile main deploy
|
|
336
|
+
|
|
337
|
+
cd ../docs
|
|
338
|
+
canopy-dev --profile main create "Canopy Docs" --slug docs --no-scaffold
|
|
339
|
+
canopy-dev --profile main deploy
|
|
340
|
+
|
|
341
|
+
cd ../../examples/kitchen-sink
|
|
342
|
+
canopy-dev --profile main create "Kitchen Sink" --slug kitchen-sink --no-scaffold
|
|
343
|
+
canopy-dev --profile main deploy
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`canopy.json` already carries the slug, build command, dist dir, and a
|
|
347
|
+
usual `"profile": "main"` pin, so `deploy` builds them — no separate
|
|
348
|
+
`pnpm build` needed. Still pass `--profile` for the intended site so a
|
|
349
|
+
stray active profile cannot send the deploy elsewhere
|
|
350
|
+
(docs/10-cli-sites.md). On a site without worker support
|
|
351
|
+
(`capabilities.workers = false`), a deploy that keeps
|
|
352
|
+
`"worker": "worker/index.ts"` will 501 `not_supported_on_target` — strip
|
|
353
|
+
the `worker` key for an assets-only deploy there; that's still enough to
|
|
354
|
+
exercise `db` / `blob` / `ai.chat` through the platform data plane.
|
|
355
|
+
|
|
356
|
+
### 11. AI Gateway keys + spend rule (script)
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
cd infra # adjust if you're coming from examples/kitchen-sink
|
|
360
|
+
set -a && source .env.runtime && set +a
|
|
361
|
+
# CF_API_TOKEN has AI Gateway Read (use that for the logs check);
|
|
362
|
+
# the script's prompt is about dashboard BYOK / spend-limit clicks.
|
|
363
|
+
export CLOUDFLARE_API_TOKEN="$CF_API_TOKEN"
|
|
364
|
+
CANOPY_ACCOUNT_ID="$(tofu output -raw account_id)" DEFAULT_BUDGET_USD=25 \
|
|
365
|
+
./scripts/bootstrap-ai-gateway.sh
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Verifies the gateway, then walks you through adding BYOK provider keys and the
|
|
369
|
+
per-project spend-limit rule (both dashboard steps — see the script's notes).
|
|
370
|
+
`AI_GATEWAY_TOKEN` alone is **Run**-scoped and cannot read logs; use
|
|
371
|
+
`CF_API_TOKEN` (D1 Write + AI Gateway Read) for the read-back check.
|
|
372
|
+
|
|
373
|
+
The hard daily budget cap is enforced by canopy-api's own `ai_spend` ledger on
|
|
374
|
+
every request; the AI Gateway spend rule is belt-and-braces on top.
|
|
375
|
+
|
|
376
|
+
### 12. Smoke test
|
|
377
|
+
|
|
378
|
+
Anonymous visits to project hosts **302 to OneLogin** by design (monk SSO;
|
|
379
|
+
same as the old `thanarie.dev` setup). Client-password mode is the only
|
|
380
|
+
anonymous path. Smoke through a real browser session:
|
|
381
|
+
|
|
382
|
+
1. `https://dashboard.canopy.website` → OneLogin login works; the project list
|
|
383
|
+
shows all four under **Core projects**.
|
|
384
|
+
2. `https://www.canopy.website` → marketing site + `/changelog` render (after
|
|
385
|
+
SSO).
|
|
386
|
+
3. `https://kitchen-sink.canopy.website` → exercise `db`, `blob`, and `ai.chat`
|
|
387
|
+
(proves per-project D1 provisioning, R2 prefixes, and the AI Gateway path —
|
|
388
|
+
BYOK must be set first for `ai.chat`).
|
|
389
|
+
4. Confirm kitchen-sink's LLM spend appears in its dashboard usage view (the
|
|
390
|
+
view reads the platform's own `ai_spend` ledger, written at call time — the
|
|
391
|
+
same rows the daily budget cap enforces).
|
|
392
|
+
5. `canopy-dev create scratch-…` then `canopy-dev delete <slug> --yes` →
|
|
393
|
+
confirms provisioning and full teardown.
|
|
394
|
+
|
|
395
|
+
CLI sanity (no browser): `curl -sS https://auth.canopy.website/auth/login`
|
|
396
|
+
should 302 to OneLogin; `curl -sS https://canopy-api.<subdomain>.workers.dev/control/v1/docs`
|
|
397
|
+
should 401 `unauthenticated` (API up, auth required).
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Teardown of the old account
|
|
402
|
+
|
|
403
|
+
Once the new account is verified and you no longer need the old projects'
|
|
404
|
+
data, delete resources in the old account (or leave it to lapse). Site
|
|
405
|
+
bindings live in `wrangler.main.jsonc` / `wrangler.staging.jsonc`; local
|
|
406
|
+
`wrangler.jsonc` is not a deploy target.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Migrations & Drizzle
|
|
3
|
+
description: Evolve your own tables with versioned SQL migrations — generated by Drizzle or written by hand — applied safely by canopy deploy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The document API (`canopy.db.collection(…)`) needs no schema and no migrations —
|
|
7
|
+
if it covers your project, skip this page. But developers who define their own
|
|
8
|
+
tables through raw `canopy.db.sql()` eventually need to _change_ those tables,
|
|
9
|
+
and that's what migrations are for: versioned `.sql` files in your repo,
|
|
10
|
+
applied exactly once each, in order, by the platform.
|
|
11
|
+
|
|
12
|
+
[Drizzle](https://orm.drizzle.team) is the recommended way to produce those
|
|
13
|
+
files (a typed schema plus generated SQL), but the mechanism is ORM-agnostic —
|
|
14
|
+
hand-written `.sql` files work identically.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
1. Your `canopy.json` names a migrations directory:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"slug": "my-app",
|
|
23
|
+
"dialect": "sqlite",
|
|
24
|
+
"migrations": "./migrations"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. The directory holds ordered `.sql` files (`0000_init.sql`, `0001_add_email.sql`, …),
|
|
29
|
+
applied in filename order.
|
|
30
|
+
|
|
31
|
+
3. The platform journals each applied file in the project database, so a file
|
|
32
|
+
applies **exactly once**, and takes an automatic **pre-migrate snapshot**
|
|
33
|
+
before applying anything — a bad migration is one
|
|
34
|
+
`canopy db restore` away.
|
|
35
|
+
|
|
36
|
+
4. Three surfaces apply them:
|
|
37
|
+
- **`canopy deploy`** — the main path. Pending migrations apply after the new
|
|
38
|
+
bundle is staged and before it goes live, so schema and code ship
|
|
39
|
+
together and the new code never sees the old schema. A failure aborts the
|
|
40
|
+
deploy with the old version still live.
|
|
41
|
+
- **`canopy dev --local`** — the sandbox applies pending migrations on boot,
|
|
42
|
+
so local dev always matches the repo.
|
|
43
|
+
- **`canopy db migrate [--plan]`** — apply (or dry-run) without deploying.
|
|
44
|
+
|
|
45
|
+
Remote-first `canopy dev` never applies migrations — it points at the
|
|
46
|
+
production database, and schema changes belong to deploys. While a schema
|
|
47
|
+
change is in flux, develop against `--local` (add `--pull` for a copy of real
|
|
48
|
+
data).
|
|
49
|
+
|
|
50
|
+
## The rules
|
|
51
|
+
|
|
52
|
+
- **Never edit an applied migration.** The platform hashes each file and
|
|
53
|
+
refuses edited history — write a new migration instead. (This is how every
|
|
54
|
+
mainstream migration tool works.)
|
|
55
|
+
- **Prefer additive changes** (new tables, new nullable columns), and do
|
|
56
|
+
removals in a _later_ release once nothing references the old shape. This
|
|
57
|
+
"expand/contract" habit makes every edge case boring.
|
|
58
|
+
- **Don't touch `_canopy_*` tables.** They're the platform's (the document
|
|
59
|
+
store, the migration journal) — the platform rejects migrations that
|
|
60
|
+
reference them.
|
|
61
|
+
- **There are no down-migrations.** Roll forward with a new migration, or
|
|
62
|
+
restore the automatic pre-migrate snapshot.
|
|
63
|
+
|
|
64
|
+
## Setting up Drizzle
|
|
65
|
+
|
|
66
|
+
Pin your dialect first — a Drizzle schema is written for one SQL flavor. Check
|
|
67
|
+
what your project's site runs on (`await canopy.db.dialect()` or the
|
|
68
|
+
[data guide](https://docs.canopy.website/guides/data)) and set `"dialect"` in `canopy.json`: `"sqlite"`
|
|
69
|
+
on Cloudflare-backed sites, `"postgres"` on GCP-backed ones. That same field
|
|
70
|
+
makes `canopy dev --local` run a matching local engine.
|
|
71
|
+
|
|
72
|
+
Install the tools and describe your schema:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install drizzle-orm
|
|
76
|
+
npm install -D drizzle-kit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts title="src/schema.ts"
|
|
80
|
+
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
81
|
+
|
|
82
|
+
export const users = sqliteTable("users", {
|
|
83
|
+
id: text("id").primaryKey(),
|
|
84
|
+
email: text("email").notNull(),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ts title="drizzle.config.ts"
|
|
89
|
+
import { defineConfig } from "drizzle-kit";
|
|
90
|
+
|
|
91
|
+
export default defineConfig({
|
|
92
|
+
dialect: "sqlite", // match canopy.json "dialect" ("postgresql" for postgres)
|
|
93
|
+
schema: "./src/schema.ts",
|
|
94
|
+
out: "./migrations",
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
(On a postgres site, import from `drizzle-orm/pg-core` and set
|
|
99
|
+
`dialect: "postgresql"` — everything else is identical.)
|
|
100
|
+
|
|
101
|
+
Generate a migration whenever the schema changes:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx drizzle-kit generate
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This is fully local — it diffs your schema files and writes `.sql` into
|
|
108
|
+
`./migrations`. **No database connection is involved, ever.** Then
|
|
109
|
+
`canopy deploy` (or `canopy db migrate`) applies it.
|
|
110
|
+
|
|
111
|
+
## Querying with Drizzle
|
|
112
|
+
|
|
113
|
+
Canopy never hands out connection strings — Drizzle talks to your data through
|
|
114
|
+
the handles Canopy already provides.
|
|
115
|
+
|
|
116
|
+
**In a project worker on a Cloudflare-backed site**, `env.DB` is a real D1
|
|
117
|
+
handle:
|
|
118
|
+
|
|
119
|
+
```ts title="worker/index.ts"
|
|
120
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
121
|
+
import { users } from "../src/schema.ts";
|
|
122
|
+
|
|
123
|
+
export default {
|
|
124
|
+
async fetch(request, env) {
|
|
125
|
+
const db = drizzle(env.DB);
|
|
126
|
+
const all = await db.select().from(users);
|
|
127
|
+
return Response.json(all);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**In the browser (or anywhere the SDK runs)**, wrap `canopy.db.sql()` with
|
|
133
|
+
Drizzle's proxy driver:
|
|
134
|
+
|
|
135
|
+
```ts title="src/db.ts"
|
|
136
|
+
import { drizzle } from "drizzle-orm/sqlite-proxy";
|
|
137
|
+
import { canopy } from "./canopy.ts";
|
|
138
|
+
|
|
139
|
+
export const db = drizzle(async (sql, params, method) => {
|
|
140
|
+
const rows = await canopy.db.sql<Record<string, unknown>>(sql, params);
|
|
141
|
+
const values = rows.map((row) => Object.values(row));
|
|
142
|
+
return { rows: method === "get" ? (values[0] ?? []) : values };
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
(On a postgres site use `drizzle-orm/pg-proxy` the same way; in a GCP project
|
|
147
|
+
worker, `env.DB.query(text, params)` fits `pg-proxy` directly.)
|
|
148
|
+
|
|
149
|
+
## Rehearsing on staging
|
|
150
|
+
|
|
151
|
+
The safest way to try a migration is on the project's staging channel — a
|
|
152
|
+
private copy of the app whose database starts as a clone of live:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
canopy deploy --staging # pending migrations apply to the STAGING database
|
|
156
|
+
# … verify against the migrated copy …
|
|
157
|
+
canopy promote # the same migrations replay against live, then the version flips
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A migration that fails on staging costs nothing; at promote time the live
|
|
161
|
+
database gets its own pre-promote snapshot before anything applies.
|
|
162
|
+
`canopy db migrate --staging` (and `--plan`) target the staging database
|
|
163
|
+
directly, and `canopy staging sync` re-clones live when you want to rehearse
|
|
164
|
+
again on fresh data. See [Staging & promoting](https://docs.canopy.website/guides/staging).
|
|
165
|
+
|
|
166
|
+
## What deliberately doesn't work
|
|
167
|
+
|
|
168
|
+
- **`drizzle-kit push` / `migrate` / `studio` / `introspect`** — these need a
|
|
169
|
+
database connection, which Canopy never exposes. Generating is local;
|
|
170
|
+
applying is Canopy's job. Don't invent a `DATABASE_URL` — there isn't one.
|
|
171
|
+
For browsing data, use the dashboard's data browser or `canopy db pull`.
|
|
172
|
+
- **Drizzle's runtime `migrate()`** — the platform journals migrations in its
|
|
173
|
+
own `_canopy_migrations` table; running Drizzle's applier would double-apply.
|
|
174
|
+
- **Cross-dialect portability.** A project with a Drizzle schema is pinned to
|
|
175
|
+
its dialect. The document API remains the portable path.
|
|
176
|
+
|
|
177
|
+
## Coexistence
|
|
178
|
+
|
|
179
|
+
Your Drizzle tables live alongside the document store in the same project
|
|
180
|
+
database. Snapshots, restore, `canopy db pull`, and the graduation export all
|
|
181
|
+
operate on the whole database, so they cover your tables automatically — and
|
|
182
|
+
since your schema history is in the repo and the journal travels with the
|
|
183
|
+
data, a graduated project takes its full migration story with it.
|