@mediamonks/canopy-cli 0.5.1 → 0.7.0

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 (43) hide show
  1. package/dist/{db-BW1ZolWn.mjs → db-BkUmbGgx.mjs} +71 -15
  2. package/dist/{deploy-alC2--pZ.mjs → deploy-BcczXlSQ.mjs} +106 -8
  3. package/dist/dev-CvtoGFW0.mjs +2 -0
  4. package/dist/{dev-CQQhMOoD.mjs → dev-DvyKHe2d.mjs} +44 -14
  5. package/dist/docs/ci.md +1 -3
  6. package/dist/docs/cli.md +3 -4
  7. package/dist/docs/data.md +5 -4
  8. package/dist/docs/deploy.md +16 -4
  9. package/dist/docs/dev.md +13 -11
  10. package/dist/docs/getting-started.md +1 -3
  11. package/dist/docs/infrastructure.md +27 -15
  12. package/dist/docs/migrations.md +183 -0
  13. package/dist/docs/porting-nextjs.md +232 -0
  14. package/dist/docs/sdk-ai.md +103 -0
  15. package/dist/docs/sdk-auth.md +57 -0
  16. package/dist/docs/sdk-database.md +126 -0
  17. package/dist/docs/sdk-files.md +82 -0
  18. package/dist/docs/sdk-services.md +58 -0
  19. package/dist/docs/sdk.md +113 -0
  20. package/dist/docs/staging.md +98 -0
  21. package/dist/docs/static-serving.md +4 -1
  22. package/dist/docs/troubleshooting.md +1 -3
  23. package/dist/{docs-DvSPpSIR.mjs → docs-2cUEug69.mjs} +23 -10
  24. package/dist/index.mjs +22 -20
  25. package/dist/{local-C5-GWJcx.mjs → local-C47tmhl7.mjs} +365 -41
  26. package/dist/{login-BO6abf40.mjs → login-ByD0hcxu.mjs} +5 -3
  27. package/dist/migrations-I1adj0z2.mjs +50 -0
  28. package/dist/{open-DnlYDqrU.mjs → open-DD9oa-Ho.mjs} +11 -4
  29. package/dist/{package-CHoZKzaw.mjs → package-CDB-uxy3.mjs} +1 -1
  30. package/dist/{projects-ijfe7Ffx.mjs → projects-CSH7KsPi.mjs} +17 -9
  31. package/dist/{site-BJjoePQS.mjs → site-BUlQR117.mjs} +14 -1
  32. package/dist/skill/canopy/SKILL.md +24 -1
  33. package/dist/{skill-CW0QSbAJ.mjs → skill-DrWzmUMv.mjs} +2 -2
  34. package/dist/staging-CfF2-pHs.mjs +113 -0
  35. package/dist/templates/spa/AGENTS.md +4 -0
  36. package/dist/templates/static/AGENTS.md +4 -0
  37. package/dist/templates/worker/AGENTS.md +7 -0
  38. package/dist/{token-BeF9zczg.mjs → token-nieJV1w7.mjs} +2 -2
  39. package/dist/{ui-DThlyHgo.mjs → ui-D8HxX0Ng.mjs} +3 -2
  40. package/dist/{upgrade-CGz1yzsW.mjs → upgrade-CH_jHkc7.mjs} +2 -2
  41. package/dist/{usage-DQpH7A3X.mjs → usage-qqPtkDtT.mjs} +2 -2
  42. package/package.json +2 -2
  43. package/dist/dev-BJ5xEwqW.mjs +0 -2
@@ -1,4 +1,7 @@
1
- # Infrastructure as code & standing up a Cloudflare account
1
+ ---
2
+ title: Infrastructure as code
3
+ description: Terraform for the account-level Cloudflare pieces Canopy needs.
4
+ ---
2
5
 
3
6
  Canopy's Cloudflare footprint is small on purpose: almost everything
4
7
  per-project is provisioned at runtime by canopy-api (`apps/api/src/cf.ts`,
@@ -19,8 +22,8 @@ Scope of a fresh stand-up:
19
22
 
20
23
  1. the **platform workers** — `canopy-api` and `canopy-gateway` — deployed and
21
24
  healthy (these aren't optional; nothing works without them), and
22
- 2. the **three core projects** — `dashboard`, `www`, `kitchen-sink` — re-created
23
- and deployed _on_ the platform like any other project.
25
+ 2. the **four core projects** — `dashboard`, `www`, `docs`, `kitchen-sink` —
26
+ re-created and deployed _on_ the platform like any other project.
24
27
 
25
28
  Everyone else's projects are **not** carried over — a new account starts with
26
29
  an empty control-plane registry (see [Non-goals](#non-goals)).
@@ -39,8 +42,8 @@ config file per Cloudflare site (same names as CLI profiles). Deployed with
39
42
  is local `wrangler dev` only.
40
43
 
41
44
  **`canopy deploy`** (`canopy.json` in each project): the core projects
42
- (`dashboard`, `www`, `kitchen-sink`) — ordinary platform projects that
43
- dogfood the product, not infrastructure.
45
+ (`dashboard`, `www`, `docs`, `kitchen-sink`) — ordinary platform projects
46
+ that dogfood the product, not infrastructure.
44
47
 
45
48
  **Bootstrap scripts** (`infra/scripts/`): runtime API tokens, worker secrets,
46
49
  AI Gateway BYOK keys + spend rule.
@@ -53,9 +56,9 @@ enable paid Workers products. Plus OneLogin: redirect URIs for the new host
53
56
  ## Non-goals
54
57
 
55
58
  - **No project-data migration.** Per-project D1s, R2 blobs, and deploy history
56
- stay in the old account. The new control-plane D1 is empty; the three core
57
- projects are re-created from scratch (they hold no precious data — `www` and
58
- `dashboard` are stateless, `kitchen-sink` is a demo), and everyone else's
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
59
62
  projects are re-created as needed. (If that ever changes, the export/import
60
63
  primitives already exist in `apps/api/src/cf.ts` and `canopy db snapshot|restore`.)
61
64
  - **No Tofu management of Workers/secrets/per-project resources** — see the
@@ -95,10 +98,15 @@ export CLOUDFLARE_ACCOUNT_ID="<account_id>" # required when you belong to >1 a
95
98
  (You can confirm via API once the bootstrap token exists — an empty zone is
96
99
  fine.)
97
100
  4. **R2** must work (creating a probe bucket is enough to confirm). **Workers
98
- Paid** and **Workers for Platforms** are _not_ required for the current CF
99
- platform path (no dispatch namespaces; `capabilities.workers` is false).
100
- Enable them later when you actually need paid Worker limits or Phase-2
101
- per-project Workers. **No Advanced Certificate Manager** — `*.canopy.website`
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`
102
110
  is a single-level wildcard covered by free Universal SSL. (ACM would only
103
111
  be needed if nesting as `*.canopy.<some-other-domain>`.)
104
112
 
@@ -283,8 +291,8 @@ If you already configured these for `canopy.website` before the move, skip.
283
291
 
284
292
  ### 10. Log in, grant yourself admin, then deploy the core projects
285
293
 
286
- The three core projects are ordinary platform projects that dogfood the product.
287
- Two of their slugs (`dashboard`, `www`) are in `RESERVED_SLUGS`
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`
288
296
  (`apps/api/src/routes/projects.ts`), so **only an admin can create them** —
289
297
  hence the ordering here.
290
298
 
@@ -326,6 +334,10 @@ export CANOPY_PROJECT_PROTOCOL="https"
326
334
  canopy-dev --profile main create "Canopy" --slug www --no-scaffold
327
335
  canopy-dev --profile main deploy
328
336
 
337
+ cd ../docs
338
+ canopy-dev --profile main create "Canopy Docs" --slug docs --no-scaffold
339
+ canopy-dev --profile main deploy
340
+
329
341
  cd ../../examples/kitchen-sink
330
342
  canopy-dev --profile main create "Kitchen Sink" --slug kitchen-sink --no-scaffold
331
343
  # CF target: capabilities.workers = false — strip worker for this deploy
@@ -371,7 +383,7 @@ same as the old `thanarie.dev` setup). Client-password mode is the only
371
383
  anonymous path. Smoke through a real browser session:
372
384
 
373
385
  1. `https://dashboard.canopy.website` → OneLogin login works; the project list
374
- shows all three under **Core projects**.
386
+ shows all four under **Core projects**.
375
387
  2. `https://www.canopy.website` → marketing site + `/changelog` render (after
376
388
  SSO).
377
389
  3. `https://kitchen-sink.canopy.website` → exercise `db`, `blob`, and `ai.chat`
@@ -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.
@@ -0,0 +1,232 @@
1
+ ---
2
+ title: Porting a Next.js app
3
+ description: How to restructure a Next.js (App Router) project so it runs on Canopy.
4
+ ---
5
+
6
+ Canopy can host a project that started life as a Next.js app — but not by
7
+ running Next.js's server. This guide explains why, and walks through the
8
+ restructuring: a **static frontend** plus an optional **backend worker on
9
+ `/api/*`**, with the platform taking over auth, data, files, and AI.
10
+
11
+ If your app barely uses Next's server side, the port is a day or two of work.
12
+ If it leans heavily on server components, server actions, or ISR, read
13
+ [When not to port](#when-not-to-port) first.
14
+
15
+ ## Why a lift-and-shift doesn't work
16
+
17
+ Canopy deliberately hosts no arbitrary servers — that's what keeps a thousand
18
+ idle projects free to run and safe to operate. Three consequences matter for
19
+ Next.js:
20
+
21
+ - **Every page is a static file.** The platform serves your deployed build
22
+ from storage; requests to `/`, `/about`, or any other page route never reach
23
+ project code (see [How static files are served](https://docs.canopy.website/guides/static-serving)).
24
+ Server-side rendering, React Server Components resolved per request,
25
+ and Incremental Static Regeneration have nowhere to run.
26
+ - **The backend is one worker, reachable only under `/api/*`.** A project can
27
+ ship a single bundled module exporting `fetch()` (and optionally
28
+ `scheduled()`), and the platform routes `/api/*` requests to it. Server
29
+ actions — which POST to page routes — never reach it.
30
+ - **Auth belongs to the platform.** Every request is already behind OneLogin
31
+ (or a project password) before your code runs. NextAuth, auth middleware,
32
+ and session handling come out of the ported app entirely.
33
+
34
+ So the port is a restructure: the frontend becomes a static build, and
35
+ whatever genuinely needs a server moves into the worker.
36
+
37
+ ## What a ported app looks like
38
+
39
+ ```
40
+ my-app/
41
+ ├── canopy.json ← slug, build command, dist folder, worker entry
42
+ ├── src/ ← frontend (static build)
43
+ ├── worker/
44
+ │ └── index.ts ← backend: fetch handler for /api/*
45
+ └── dist/ ← build output that gets deployed
46
+ ```
47
+
48
+ ```json
49
+ {
50
+ "slug": "my-app",
51
+ "build": "npm run build",
52
+ "dist": "dist",
53
+ "worker": "worker/index.ts"
54
+ }
55
+ ```
56
+
57
+ One `canopy deploy` builds and ships both; frontend and backend always move
58
+ together as a single version, with rollback.
59
+
60
+ ## Step 1 — take inventory
61
+
62
+ List everything in the app that runs on the server. Each item lands in one of
63
+ three buckets:
64
+
65
+ | Next.js feature | Where it goes on Canopy |
66
+ | ----------------------------------------------- | -------------------------------------------------------------------------------- |
67
+ | Pages, layouts, client components | Static build (unchanged in spirit) |
68
+ | Server components fetching data at request time | Client-side fetching, or a worker endpoint |
69
+ | Route handlers (`app/api/*/route.ts`) | Worker endpoints under `/api/*` |
70
+ | Server actions | Explicit worker endpoints, called with `fetch` from the client |
71
+ | Middleware | Usually deleted (auth is platform-provided); anything else moves into the worker |
72
+ | NextAuth / auth middleware | Deleted — use `canopy.auth.user()` (client) or `env.USER` (worker) |
73
+ | Database (Prisma, Postgres, …) | `canopy.db` — per-project database, document API + SQL escape hatch |
74
+ | File uploads (S3, …) | `canopy.blob` — per-project file storage with signed downloads |
75
+ | AI SDK / OpenAI client | `canopy.ai.chat()`, or point the Vercel AI SDK at `canopy.ai.endpoint()` |
76
+ | `NEXT_PUBLIC_*` env vars | Build-time env vars in your CI — the platform doesn't inject build config |
77
+ | Server-side env vars / secrets | Project secrets (dashboard Settings) — injected into the worker's `env` |
78
+ | Cron (`vercel.json` crons) | [Schedules](https://docs.canopy.website/platform/schedules) — delivered to the worker's `scheduled()` |
79
+ | ISR / `revalidate` / caching | Dropped — at internal-tool scale, fetch fresh or cache in the client |
80
+ | `next/image` optimization | Plain `<img>`, or optimize at build time |
81
+ | Custom domain | Not available — projects live at `<slug>.canopy.<domain>` behind sign-in |
82
+ | Queues / background jobs | Not supported — restructure around request/response plus schedules |
83
+
84
+ If the last two rows are deal-breakers, stop here: see
85
+ [When not to port](#when-not-to-port).
86
+
87
+ ## Step 2 — make the frontend static
88
+
89
+ Two routes, pick one:
90
+
91
+ **Static export (quickest).** Set `output: 'export'` in `next.config.js` and
92
+ fix what breaks. You keep the App Router file layout and components, but lose
93
+ everything request-time: no route handlers, no server actions, no dynamic
94
+ route segments without `generateStaticParams`, no request-time server
95
+ components (they still run — but once, at build time). `next build` writes a
96
+ fully static site to `out/`; point `dist` at it. Set
97
+ `images: { unoptimized: true }` since there's no image optimizer serving
98
+ requests.
99
+
100
+ **Rebuild as a Vite SPA (more idiomatic).** Canopy's templates and tooling
101
+ assume a single-page app: one `index.html`, client-side routing, `canopy dev`
102
+ for local development against real project data. If you're porting an app
103
+ you'll keep evolving on Canopy — especially with an AI agent doing the work —
104
+ converting pages to a React + Vite SPA usually pays for itself quickly, and
105
+ agents handle this conversion well. Page routes become client routes (the
106
+ platform serves `index.html` for unknown paths, so deep links work).
107
+
108
+ Either way, data that pages used to fetch on the server now arrives one of two
109
+ ways: fetched from the client through the SDK (`canopy.db`, `canopy.blob` —
110
+ fine for most internal tools), or from your worker via `/api/*` when it needs
111
+ server-side logic or secrets.
112
+
113
+ ## Step 3 — move the backend into a worker
114
+
115
+ Route handlers and server actions become endpoints in one fetch handler.
116
+ [Hono](https://hono.dev) is a natural fit and bundles cleanly:
117
+
118
+ ```ts
119
+ // worker/index.ts
120
+ import { createCanopy } from "@mediamonks/canopy";
121
+ import { Hono } from "hono";
122
+
123
+ type Env = {
124
+ CANOPY: { fetch: typeof fetch };
125
+ /** Signed-in monk, verified by the platform; null for cron. */
126
+ USER: { sub: string; email: string; name: string } | null;
127
+ /** Project secrets from dashboard Settings appear as plain keys. */
128
+ MY_API_KEY?: string;
129
+ };
130
+
131
+ const app = new Hono<{ Bindings: Env }>();
132
+
133
+ // was: app/api/report/route.ts
134
+ app.post("/api/report", async (c) => {
135
+ const user = c.env.USER;
136
+ if (!user) return c.json({ error: "unauthenticated" }, 401);
137
+
138
+ // The SDK works inside the worker through the CANOPY binding —
139
+ // same database and files the frontend sees, caller's identity attached.
140
+ const canopy = createCanopy({ fetch: c.env.CANOPY.fetch.bind(c.env.CANOPY) });
141
+ const rows = await canopy.db.collection("reports").find({ where: { owner: user.email } });
142
+ return c.json({ rows });
143
+ });
144
+
145
+ // was: a server action
146
+ app.post("/api/submit", async (c) => {
147
+ const body = await c.req.json();
148
+ // …validate, write via canopy.db, call an external API with c.env.MY_API_KEY…
149
+ return c.json({ ok: true });
150
+ });
151
+
152
+ export default {
153
+ fetch: app.fetch.bind(app),
154
+ // was: vercel.json crons — configure in dashboard Settings → Schedules
155
+ async scheduled(event: { name: string }, env: Env) {
156
+ if (event.name === "nightly-report") {
157
+ // …
158
+ }
159
+ },
160
+ };
161
+ ```
162
+
163
+ Rules of the road for the worker:
164
+
165
+ - It's **one bundled file** — dependencies are bundled in at deploy time, so
166
+ stick to packages that don't need Node built-ins or native modules.
167
+ - **No filesystem, no long-lived state.** Persist through `canopy.db` and
168
+ `canopy.blob`.
169
+ - **Outbound requests are controlled.** Calls to external APIs pass through a
170
+ platform-managed egress allowlist — if your project needs to call an
171
+ external host, ask the platform team to allow it.
172
+ - Frontend calls are plain `fetch("/api/…")` — same origin, the platform
173
+ session cookie rides along automatically.
174
+
175
+ ## Step 4 — swap the data layer
176
+
177
+ Replace the ORM and connection strings with the project's own database:
178
+
179
+ - The paved road is the [document API](https://docs.canopy.website/sdk/database):
180
+ `canopy.db.collection("todos").insert(…)` / `find(…)` — schemaless JSON, no
181
+ migrations. For most internal tools this replaces Prisma models outright.
182
+ - Relational shapes port via the SQL escape hatch, `canopy.db.sql()`, against
183
+ the same per-project database.
184
+ - Existing data can be imported once: export from your old database, then load
185
+ it through the worker or the [SQL console](https://docs.canopy.website/guides/data) in the dashboard.
186
+
187
+ Files move to [`canopy.blob`](https://docs.canopy.website/sdk/files) (uploads, listings, short-lived
188
+ signed download URLs — nothing is ever publicly readable). AI calls move to
189
+ [`canopy.ai`](https://docs.canopy.website/sdk/ai); if the app uses the Vercel AI SDK, point it at
190
+ `canopy.ai.endpoint()` and it keeps working, now metered and budget-capped.
191
+
192
+ ## Step 5 — delete the auth
193
+
194
+ This is the satisfying part. OneLogin fronts every request before your code
195
+ runs, so the ported app removes sign-in pages, session handling, and auth
196
+ middleware entirely:
197
+
198
+ - **Client:** `canopy.auth.user()` returns the signed-in monk.
199
+ - **Worker:** `env.USER` carries the verified identity (no round-trip).
200
+ - Client-facing access without OneLogin is a per-project
201
+ [password](https://docs.canopy.website/platform/client-access), configured in Settings — not code.
202
+
203
+ Authorization (who may do what _inside_ the app) stays yours, but it now keys
204
+ off a verified identity instead of a homegrown session.
205
+
206
+ ## When not to port
207
+
208
+ Be honest about fit before starting. Dedicated hosting is the better home
209
+ when the app:
210
+
211
+ - depends on **per-request SSR** for real reasons — SEO on public pages,
212
+ personalization at the edge, streaming server components as a core UX;
213
+ - is built around **server actions and RSC data flow** so thoroughly that the
214
+ restructure is a rewrite;
215
+ - needs a **custom domain or anonymous public access** — nothing on Canopy is
216
+ reachable without OneLogin or a project password;
217
+ - needs **queues, background jobs, or long-running work**.
218
+
219
+ That's not a workaround waiting to happen — it's the platform's design. Canopy
220
+ optimizes for small internal tools; an app that genuinely needs a server is
221
+ past greenhouse size (see [Use cases](https://docs.canopy.website/overview/use-cases)).
222
+
223
+ ## Checklist
224
+
225
+ - [ ] Inventory server-side surface (RSC fetching, actions, route handlers, middleware, crons)
226
+ - [ ] Frontend: `output: 'export'` or Vite SPA; fix dynamic segments and `next/image`
227
+ - [ ] Worker: route handlers + server actions → `/api/*` endpoints in `worker/index.ts`
228
+ - [ ] Data: ORM → `canopy.db`; uploads → `canopy.blob`; AI clients → `canopy.ai`
229
+ - [ ] Delete NextAuth/middleware; read identity from `canopy.auth.user()` / `env.USER`
230
+ - [ ] Secrets into dashboard Settings; `NEXT_PUBLIC_*` into your CI build
231
+ - [ ] Crons into Settings → Schedules, handled in `scheduled()`
232
+ - [ ] `canopy.json` with `build`, `dist`, `worker` — then `canopy deploy`
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: AI
3
+ description: Metered LLM access through the project's budget-capped gateway key — chat, streaming, and an OpenAI-compatible endpoint.
4
+ ---
5
+
6
+ `canopy.ai` gives your project LLM access without any API keys in your code.
7
+ Every request authenticates as the project and is metered against a hard
8
+ per-project daily budget — spend and limits are covered in
9
+ [AI access & usage](https://docs.canopy.website/platform/ai-usage).
10
+
11
+ ## ai.chat(options)
12
+
13
+ One-shot chat completion. Takes a `ChatOptions` object, resolves to a
14
+ `ChatResponse`:
15
+
16
+ ```ts
17
+ const res = await canopy.ai.chat({
18
+ messages: [
19
+ { role: "system", content: "You are terse." },
20
+ { role: "user", content: "Say hi in five words." },
21
+ ],
22
+ model: "anthropic/claude-haiku-4-5", // optional
23
+ temperature: 0.7, // optional, 0–2
24
+ maxTokens: 256, // optional
25
+ });
26
+
27
+ res.message.content; // the assistant's reply
28
+ res.model; // the model that actually answered
29
+ res.usage; // { inputTokens: number, outputTokens: number }
30
+ ```
31
+
32
+ **`ChatOptions`:**
33
+
34
+ - `messages` — required, non-empty array of `{ role, content }`. `role` is
35
+ `"system" | "user" | "assistant"`; `content` is a non-empty string.
36
+ - `model` — optional `"{provider}/{model}"` id. Omit it and the platform's
37
+ default model answers. Ask for a model whose provider isn't enabled and the
38
+ call fails with a message listing what _is_ available; `ai.endpoint()`
39
+ returns the full curated list.
40
+ - `temperature` — optional number between 0 and 2.
41
+ - `maxTokens` — optional integer, at least 1.
42
+
43
+ **`ChatResponse`:** `{ message: ChatMessage, model: string, usage: { inputTokens, outputTokens } }`.
44
+ `message.role` is `"assistant"`.
45
+
46
+ ## ai.chatStream(options)
47
+
48
+ Same `ChatOptions`, but resolves to a `ReadableStream<Uint8Array>` of
49
+ server-sent events — the raw SSE bytes of the completion, for chat-style UIs
50
+ that render tokens as they arrive:
51
+
52
+ ```ts
53
+ const stream = await canopy.ai.chatStream({
54
+ messages: [{ role: "user", content: "Count to five." }],
55
+ });
56
+
57
+ const decoder = new TextDecoder();
58
+ let text = "";
59
+ for await (const chunk of stream) {
60
+ text += decoder.decode(chunk, { stream: true });
61
+ }
62
+ ```
63
+
64
+ Each SSE event is a `data: {...}` line carrying an OpenAI-style completion
65
+ chunk. If you'd rather not parse SSE yourself, use `ai.endpoint()` below and
66
+ let an ecosystem client handle streaming for you.
67
+
68
+ ## ai.endpoint()
69
+
70
+ Returns the project's OpenAI-compatible endpoint info:
71
+
72
+ ```ts
73
+ const { baseUrl, models } = await canopy.ai.endpoint();
74
+ // baseUrl — an OpenAI-compatible base URL
75
+ // models — the curated model list, "{provider}/{model}" ids; models[0] is the default
76
+ ```
77
+
78
+ Point any OpenAI-compatible client — the Vercel AI SDK, an OpenAI SDK, a
79
+ tool-calling loop — at `baseUrl`. It uses the same project key and counts
80
+ against the same budget as `ai.chat()`:
81
+
82
+ ```ts
83
+ import { createOpenAI } from "@ai-sdk/openai";
84
+
85
+ const { baseUrl, models } = await canopy.ai.endpoint();
86
+ const openai = createOpenAI({ baseURL: baseUrl, apiKey: "unused" });
87
+ const model = openai(models[0]);
88
+ ```
89
+
90
+ ## Errors
91
+
92
+ Failed calls throw `CanopyError` with the HTTP `status` and the response body
93
+ as the `message`. Two worth handling:
94
+
95
+ - **Budget exhausted** — status `429`, body contains
96
+ `"ai_budget_exceeded"`. The project hit its daily AI cap; AI calls resume
97
+ the next day (or after a maintainer raises the budget). Everything else in
98
+ your app keeps working.
99
+ - **Invalid request** — status `400`, body contains `"invalid_chat"` and a
100
+ detail explaining which option was rejected.
101
+
102
+ > **Note:** Budgets are enforced before every call, so a runaway loop becomes a capped bill — see [AI access
103
+ > &amp; usage](https://docs.canopy.website/platform/ai-usage) for how spend is tracked and displayed.
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Auth
3
+ description: Read who the platform signed in — you never build a login screen.
4
+ ---
5
+
6
+ Authentication happens before your code runs: the platform gates every request
7
+ to your project, so by the time a page loads or a worker route fires, there is
8
+ always a signed-in user. You never build a login screen, store passwords, or
9
+ check tokens — see [Members and access](https://docs.canopy.website/platform/members-and-access) for
10
+ who gets in and how. `canopy.auth` only reports who the platform let in.
11
+
12
+ ## auth.user()
13
+
14
+ Returns the authenticated user for the current session as a `CanopyUser`:
15
+
16
+ ```ts
17
+ const user = await canopy.auth.user();
18
+ // { id: "…", email: "ada@monks.com", name: "Ada Lovelace", kind: "monk" }
19
+ ```
20
+
21
+ | Field | Type | Meaning |
22
+ | ------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
23
+ | `id` | `string` | Stable subject from OneLogin — safe to key user data on. |
24
+ | `email` | `string` | The user's email address. |
25
+ | `name` | `string` | Display name. |
26
+ | `kind` | `"monk" \| "external"` | `"monk"` = signed in through OneLogin; `"external"` = a visitor who got in with the project's client password instead. |
27
+
28
+ `kind: "external"` sessions come from client-password access — clients viewing
29
+ a shared project without a Monks account. Branch on it when externals should
30
+ see a trimmed-down experience:
31
+
32
+ ```ts
33
+ const user = await canopy.auth.user();
34
+
35
+ document.querySelector("h1")!.textContent = `Welcome, ${user.name}`;
36
+
37
+ if (user.kind === "external") {
38
+ // Client viewer: hide internal tooling, show the presentation view.
39
+ document.querySelector("#admin-panel")?.remove();
40
+ }
41
+ ```
42
+
43
+ > **Warning:** Branching on `kind` in the browser is presentation, not security — anything your page can fetch, a
44
+ > visitor can fetch. The platform's access rules are what actually protect data.
45
+
46
+ ## auth.logout()
47
+
48
+ Ends the platform session for the current visitor. Their next request hits the
49
+ login flow again:
50
+
51
+ ```ts
52
+ await canopy.auth.logout();
53
+ location.reload(); // next load re-authenticates
54
+ ```
55
+
56
+ This signs out the browser session only — it does not touch your `canopy login`
57
+ on the CLI.