@mediamonks/canopy-cli 0.7.12 → 0.7.13

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 (119) hide show
  1. package/dist/{db-8Etjmzxh.mjs → db-DKnJQPm5.mjs} +2 -2
  2. package/dist/{deploy-B1WrPq5W.mjs → deploy-CnxJ4TE6.mjs} +2 -2
  3. package/dist/dev-BqLvttKc.mjs +2 -0
  4. package/dist/{dev-BnUYGQQm.mjs → dev-CyJT6EfQ.mjs} +4 -4
  5. package/dist/docs/ci.md +50 -0
  6. package/dist/docs/cli-create.md +46 -0
  7. package/dist/docs/cli-db.md +86 -0
  8. package/dist/docs/cli-delete.md +44 -0
  9. package/dist/docs/cli-deploy.md +58 -0
  10. package/dist/docs/cli-dev.md +82 -0
  11. package/dist/docs/cli-docs.md +47 -0
  12. package/dist/docs/cli-download.md +42 -0
  13. package/dist/docs/cli-egress.md +37 -0
  14. package/dist/docs/cli-feedback.md +75 -0
  15. package/dist/docs/cli-index.md +61 -0
  16. package/dist/docs/cli-init.md +46 -0
  17. package/dist/docs/cli-login.md +43 -0
  18. package/dist/docs/cli-logout.md +32 -0
  19. package/dist/docs/cli-logs.md +57 -0
  20. package/dist/docs/cli-open.md +41 -0
  21. package/dist/docs/cli-projects.md +43 -0
  22. package/dist/docs/cli-promote.md +44 -0
  23. package/dist/docs/cli-rollback.md +45 -0
  24. package/dist/docs/cli-skill.md +39 -0
  25. package/dist/docs/cli-staging.md +54 -0
  26. package/dist/docs/cli-token.md +47 -0
  27. package/dist/docs/cli-upgrade.md +43 -0
  28. package/dist/docs/cli-usage.md +43 -0
  29. package/dist/docs/cli-whoami.md +46 -0
  30. package/dist/docs/cli.md +86 -0
  31. package/dist/docs/data.md +85 -0
  32. package/dist/docs/deploy.md +81 -0
  33. package/dist/docs/dev.md +72 -0
  34. package/dist/docs/developers-agents-and-ai.md +95 -0
  35. package/dist/docs/developers-architecture.md +106 -0
  36. package/dist/docs/getting-started.md +108 -0
  37. package/dist/docs/index.json +336 -0
  38. package/dist/docs/infrastructure.md +406 -0
  39. package/dist/docs/migrations.md +183 -0
  40. package/dist/docs/overview-faq.md +105 -0
  41. package/dist/docs/overview-how-it-works.md +82 -0
  42. package/dist/docs/overview-use-cases.md +76 -0
  43. package/dist/docs/overview-what-is-canopy.md +75 -0
  44. package/dist/docs/platform-ai-usage.md +65 -0
  45. package/dist/docs/platform-client-access.md +54 -0
  46. package/dist/docs/platform-deploys-and-rollbacks.md +93 -0
  47. package/dist/docs/platform-egress.md +75 -0
  48. package/dist/docs/platform-feedback.md +114 -0
  49. package/dist/docs/platform-google.md +213 -0
  50. package/dist/docs/platform-members-and-access.md +71 -0
  51. package/dist/docs/platform-project-data.md +76 -0
  52. package/dist/docs/platform-schedules.md +90 -0
  53. package/dist/docs/platform-sites-and-profiles.md +61 -0
  54. package/dist/docs/porting-nextjs.md +233 -0
  55. package/dist/docs/sdk-ai.md +103 -0
  56. package/dist/docs/sdk-auth.md +57 -0
  57. package/dist/docs/sdk-database.md +126 -0
  58. package/dist/docs/sdk-files.md +82 -0
  59. package/dist/docs/sdk-services.md +58 -0
  60. package/dist/docs/sdk.md +126 -0
  61. package/dist/docs/staging.md +106 -0
  62. package/dist/docs/static-serving.md +323 -0
  63. package/dist/docs/troubleshooting.md +102 -0
  64. package/dist/{docs-DxB7NhZs.mjs → docs-BBMVxVta.mjs} +2 -2
  65. package/dist/{egress-CN_axsAN.mjs → egress-BFSwa8Uu.mjs} +1 -1
  66. package/dist/{feedback-B-GXUBPs.mjs → feedback-BMX_d4KZ.mjs} +2 -2
  67. package/dist/index.mjs +25 -24
  68. package/dist/{local-BGRd_FMZ.mjs → local-BQxsp-ZQ.mjs} +2 -2
  69. package/dist/{login-D7dI8D9N.mjs → login-BDTO3nVw.mjs} +1 -1
  70. package/dist/{logs-DNiZquki.mjs → logs-C53y2eAS.mjs} +1 -1
  71. package/dist/{migrations-C-5x6SYf.mjs → migrations-C6EcKD6a.mjs} +1 -1
  72. package/dist/{open-DRPp4QJg.mjs → open-C5x3tX6I.mjs} +1 -1
  73. package/dist/{package-CDLufecy.mjs → package-vJXq4RNN.mjs} +1 -1
  74. package/dist/{projects-C6JQjVqg.mjs → projects-MMbv7MmK.mjs} +1 -1
  75. package/dist/schedule-Cq9K0rnE.mjs +106 -0
  76. package/dist/skill/canopy/SKILL.md +160 -0
  77. package/dist/{skill-BOWfqwr2.mjs → skill-Zq9aIPKf.mjs} +2 -2
  78. package/dist/{staging-Bw1D_J8M.mjs → staging-D8G4amjO.mjs} +1 -1
  79. package/dist/templates/spa/.github/workflows/canopy-deploy.yml +34 -0
  80. package/dist/templates/spa/AGENTS.md +47 -0
  81. package/dist/templates/spa/README.md +14 -0
  82. package/dist/templates/spa/_gitignore +4 -0
  83. package/dist/templates/spa/canopy.json +5 -0
  84. package/dist/templates/spa/index.html +12 -0
  85. package/dist/templates/spa/package.json +21 -0
  86. package/dist/templates/spa/src/app.tsx +25 -0
  87. package/dist/templates/spa/src/canopy.ts +5 -0
  88. package/dist/templates/spa/src/main.tsx +10 -0
  89. package/dist/templates/spa/src/style.css +29 -0
  90. package/dist/templates/spa/tsconfig.json +16 -0
  91. package/dist/templates/spa/vite.config.ts +7 -0
  92. package/dist/templates/static/AGENTS.md +45 -0
  93. package/dist/templates/static/README.md +9 -0
  94. package/dist/templates/static/_gitignore +2 -0
  95. package/dist/templates/static/canopy.json +4 -0
  96. package/dist/templates/static/index.html +21 -0
  97. package/dist/templates/static/main.js +9 -0
  98. package/dist/templates/static/style.css +29 -0
  99. package/dist/templates/worker/.dev.vars.example +9 -0
  100. package/dist/templates/worker/.github/workflows/canopy-deploy.yml +34 -0
  101. package/dist/templates/worker/AGENTS.md +63 -0
  102. package/dist/templates/worker/README.md +17 -0
  103. package/dist/templates/worker/_gitignore +5 -0
  104. package/dist/templates/worker/canopy.json +6 -0
  105. package/dist/templates/worker/index.html +12 -0
  106. package/dist/templates/worker/package.json +22 -0
  107. package/dist/templates/worker/src/app.tsx +25 -0
  108. package/dist/templates/worker/src/canopy.ts +5 -0
  109. package/dist/templates/worker/src/main.tsx +10 -0
  110. package/dist/templates/worker/src/style.css +29 -0
  111. package/dist/templates/worker/tsconfig.json +16 -0
  112. package/dist/templates/worker/vite.config.ts +7 -0
  113. package/dist/templates/worker/worker/index.ts +52 -0
  114. package/dist/{token-D7zrIF8G.mjs → token-BUxgRJVA.mjs} +1 -1
  115. package/dist/{ui-2mrJcjYC.mjs → ui-B1Px5y7q.mjs} +17 -0
  116. package/dist/{upgrade-fhfrid_O.mjs → upgrade-ES8u8_Pn.mjs} +2 -2
  117. package/dist/{usage-CX1Izj_l.mjs → usage-DjNngyvy.mjs} +1 -1
  118. package/package.json +1 -1
  119. package/dist/dev-CySBxW7I.mjs +0 -2
@@ -0,0 +1,233 @@
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
+ per-project egress allowlist — deny-by-default, managed by you. Allow the
171
+ hosts your worker calls with `canopy egress allow <host>` or in dashboard
172
+ Settings → Outbound hosts (see [Outbound requests](https://docs.canopy.website/platform/egress)).
173
+ - Frontend calls are plain `fetch("/api/…")` — same origin, the platform
174
+ session cookie rides along automatically.
175
+
176
+ ## Step 4 — swap the data layer
177
+
178
+ Replace the ORM and connection strings with the project's own database:
179
+
180
+ - The paved road is the [document API](https://docs.canopy.website/sdk/database):
181
+ `canopy.db.collection("todos").insert(…)` / `find(…)` — schemaless JSON, no
182
+ migrations. For most internal tools this replaces Prisma models outright.
183
+ - Relational shapes port via the SQL escape hatch, `canopy.db.sql()`, against
184
+ the same per-project database.
185
+ - Existing data can be imported once: export from your old database, then load
186
+ it through the worker or the [SQL console](https://docs.canopy.website/guides/data) in the dashboard.
187
+
188
+ Files move to [`canopy.blob`](https://docs.canopy.website/sdk/files) (uploads, listings, short-lived
189
+ signed download URLs — nothing is ever publicly readable). AI calls move to
190
+ [`canopy.ai`](https://docs.canopy.website/sdk/ai); if the app uses the Vercel AI SDK, point it at
191
+ `canopy.ai.endpoint()` and it keeps working, now metered and budget-capped.
192
+
193
+ ## Step 5 — delete the auth
194
+
195
+ This is the satisfying part. OneLogin fronts every request before your code
196
+ runs, so the ported app removes sign-in pages, session handling, and auth
197
+ middleware entirely:
198
+
199
+ - **Client:** `canopy.auth.user()` returns the signed-in monk.
200
+ - **Worker:** `env.USER` carries the verified identity (no round-trip).
201
+ - Client-facing access without OneLogin is a per-project
202
+ [password](https://docs.canopy.website/platform/client-access), configured in Settings — not code.
203
+
204
+ Authorization (who may do what _inside_ the app) stays yours, but it now keys
205
+ off a verified identity instead of a homegrown session.
206
+
207
+ ## When not to port
208
+
209
+ Be honest about fit before starting. Dedicated hosting is the better home
210
+ when the app:
211
+
212
+ - depends on **per-request SSR** for real reasons — SEO on public pages,
213
+ personalization at the edge, streaming server components as a core UX;
214
+ - is built around **server actions and RSC data flow** so thoroughly that the
215
+ restructure is a rewrite;
216
+ - needs a **custom domain or anonymous public access** — nothing on Canopy is
217
+ reachable without OneLogin or a project password;
218
+ - needs **queues, background jobs, or long-running work**.
219
+
220
+ That's not a workaround waiting to happen — it's the platform's design. Canopy
221
+ optimizes for small internal tools; an app that genuinely needs a server is
222
+ past greenhouse size (see [Use cases](https://docs.canopy.website/overview/use-cases)).
223
+
224
+ ## Checklist
225
+
226
+ - [ ] Inventory server-side surface (RSC fetching, actions, route handlers, middleware, crons)
227
+ - [ ] Frontend: `output: 'export'` or Vite SPA; fix dynamic segments and `next/image`
228
+ - [ ] Worker: route handlers + server actions → `/api/*` endpoints in `worker/index.ts`
229
+ - [ ] Data: ORM → `canopy.db`; uploads → `canopy.blob`; AI clients → `canopy.ai`
230
+ - [ ] Delete NextAuth/middleware; read identity from `canopy.auth.user()` / `env.USER`
231
+ - [ ] Secrets into dashboard Settings; `NEXT_PUBLIC_*` into your CI build
232
+ - [ ] Crons into Settings → Schedules, handled in `scheduled()`
233
+ - [ ] `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.
@@ -0,0 +1,126 @@
1
+ ---
2
+ title: Database
3
+ description: Document-style collections on your project's private database, plus a raw SQL escape hatch.
4
+ ---
5
+
6
+ Every project has its own private database. The paved road is `canopy.db` —
7
+ document-style collections with no tables to create and no migrations to run.
8
+ Insert a JSON document, get it back with an id and timestamps. For the rare
9
+ case where documents aren't enough, `db.sql()` runs raw SQL against the same
10
+ database.
11
+
12
+ ## Collections
13
+
14
+ ```ts
15
+ const todos = canopy.db.collection<Todo>("todos");
16
+ ```
17
+
18
+ `db.collection<T>(name)` returns a typed `Collection<T>`. The name must be
19
+ lowercase: start with a letter, then letters, digits, and underscores only
20
+ (`^[a-z][a-z0-9_]*$`). Use `snake_case` for multi-word names — `blog_posts`,
21
+ not `blogPosts`. An invalid name throws immediately.
22
+
23
+ Pass an interface as `T` to type every call end to end:
24
+
25
+ ```ts
26
+ interface Todo extends Record<string, unknown> {
27
+ title: string;
28
+ done: boolean;
29
+ }
30
+
31
+ const todos = canopy.db.collection<Todo>("todos");
32
+ ```
33
+
34
+ Every document you read back is a `Doc<T>` — your own fields plus three
35
+ platform-managed ones:
36
+
37
+ | Field | Type | Meaning |
38
+ | ------------ | -------- | ------------------------------- |
39
+ | `_id` | `string` | Stable document id |
40
+ | `_createdAt` | `string` | ISO timestamp, set on insert |
41
+ | `_updatedAt` | `string` | ISO timestamp, bumped on update |
42
+
43
+ ## Writing documents
44
+
45
+ ```ts
46
+ // insert(data: T) → Promise<Doc<T>>
47
+ const created = await todos.insert({ title: "try canopy", done: false });
48
+ created._id; // "…" — use this for get/update/delete
49
+
50
+ // update(id, patch: Partial<T>) → Promise<Doc<T>>
51
+ await todos.update(created._id, { done: true });
52
+
53
+ // delete(id) → Promise<void>
54
+ await todos.delete(created._id);
55
+ ```
56
+
57
+ `update` is a merge-patch (RFC 7386): top-level and nested objects merge into
58
+ the existing document, and setting a key to `null` removes it. Fields you
59
+ leave out of the patch are untouched.
60
+
61
+ ## Reading documents
62
+
63
+ ```ts
64
+ // get(id) → Promise<Doc<T> | null> — null when the id doesn't exist
65
+ const one = await todos.get(created._id);
66
+
67
+ // find(options?) → Promise<Doc<T>[]>
68
+ const open = await todos.find({
69
+ where: { done: false },
70
+ orderBy: "_createdAt",
71
+ order: "desc",
72
+ limit: 5,
73
+ offset: 0,
74
+ });
75
+ ```
76
+
77
+ `find` takes a `FindOptions` object; every field is optional (call
78
+ `find()` with no arguments to list everything):
79
+
80
+ | Option | Type | Meaning |
81
+ | --------- | ------------------------- | ---------------------------------------------------- |
82
+ | `where` | `Record<string, unknown>` | Filter on document fields, e.g. `{ status: "open" }` |
83
+ | `orderBy` | `string` | Field to sort on (your fields or `_createdAt` etc.) |
84
+ | `order` | `"asc" \| "desc"` | Sort direction |
85
+ | `limit` | `number` | Maximum documents to return |
86
+ | `offset` | `number` | Documents to skip (pagination) |
87
+
88
+ ## Searching text
89
+
90
+ ```ts
91
+ // search(text, options?) → Promise<Doc<T>[]>
92
+ const hits = await todos.search("canopy", { limit: 10 });
93
+ ```
94
+
95
+ `search` matches `text` against the content of every document in the
96
+ collection. On SQLite sites it is real full-text search; on Postgres sites it
97
+ falls back to substring matching — same call, same results shape, slightly
98
+ different ranking. `options` accepts everything `FindOptions` does except
99
+ `where` (`orderBy`, `order`, `limit`, `offset`).
100
+
101
+ ## Raw SQL
102
+
103
+ ```ts
104
+ // dialect() → Promise<"sqlite" | "postgres">
105
+ const dialect = await canopy.db.dialect();
106
+
107
+ // sql<Row>(query, params?) → Promise<Row[]>
108
+ const rows =
109
+ dialect === "postgres"
110
+ ? await canopy.db.sql("SELECT * FROM my_table WHERE id = $1", [42])
111
+ : await canopy.db.sql("SELECT * FROM my_table WHERE id = ?", [42]);
112
+ ```
113
+
114
+ `db.sql()` runs against the same project database, but it is **not
115
+ portable**: the query passes through untranslated in the deployment's own
116
+ dialect. That means SQLite syntax on some sites and Postgres syntax on
117
+ others — including placeholders, `?` on SQLite versus `$1` on Postgres.
118
+ `db.dialect()` tells you which one you're talking to; the result is cached
119
+ per SDK instance, so calling it often is free.
120
+
121
+ > **Note:** Reach for collections first — they work identically everywhere your project can deploy. Use
122
+ > `db.sql()` only when you genuinely need SQL (aggregates, joins, bulk updates), and branch on
123
+ > `db.dialect()` whenever the syntax differs.
124
+
125
+ Where the data lives, snapshots, and how to inspect it from the dashboard are
126
+ covered in [Project data](https://docs.canopy.website/platform/project-data).
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: Files
3
+ description: Upload, download, and list files on your project's private blob storage.
4
+ ---
5
+
6
+ `canopy.blob` stores files under your project's own isolated prefix. Nothing
7
+ is publicly accessible — every download goes through the platform with the
8
+ visitor's session, or through a short-lived signed URL you request explicitly.
9
+
10
+ ## Uploading
11
+
12
+ ```ts
13
+ // put(key, data, options?) → Promise<BlobObject>
14
+ await canopy.blob.put("notes.txt", "hello canopy", { contentType: "text/plain" });
15
+ await canopy.blob.put("avatars/arjan.png", file); // a File/Blob carries its own type
16
+ ```
17
+
18
+ `data` can be a `Blob` (including a `File`), an `ArrayBuffer`, or a `string`.
19
+ The content type is resolved in order: the explicit `options.contentType`, then
20
+ the `Blob`'s own `type`, then `application/octet-stream`. Keys are plain
21
+ strings — use `/`-separated segments (`avatars/arjan.png`) so related files
22
+ share a prefix you can `list()` later. Putting to an existing key overwrites it.
23
+
24
+ Every upload (and every `list` entry) is described by a `BlobObject`:
25
+
26
+ | Field | Type | Meaning |
27
+ | ------------- | -------- | ---------------------------- |
28
+ | `key` | `string` | The key you stored it under |
29
+ | `size` | `number` | Size in bytes |
30
+ | `contentType` | `string` | MIME type it was stored with |
31
+ | `uploadedAt` | `string` | ISO timestamp of the upload |
32
+
33
+ ## Downloading
34
+
35
+ ```ts
36
+ // get(key) → Promise<Blob | null> — null when the key doesn't exist
37
+ const blob = await canopy.blob.get("notes.txt");
38
+ const text = await blob?.text();
39
+
40
+ // getUrl(key) → Promise<{ url: string; expiresAt: string }>
41
+ const { url, expiresAt } = await canopy.blob.getUrl("avatars/arjan.png");
42
+ ```
43
+
44
+ `get` returns the content as a standard `Blob`, typed with the MIME type it
45
+ was uploaded with — use `.text()` for text, `.arrayBuffer()` or `.stream()`
46
+ for processing, or `URL.createObjectURL(blob)` for an `<img src>`.
47
+
48
+ `getUrl` returns a short-lived download URL served through the platform (the
49
+ signature alone grants access, so no cookies are needed). Prefer it when the
50
+ browser should fetch the bytes itself — image `src` attributes, download
51
+ links, media elements — and prefer `get` when your code needs the content.
52
+ Request a fresh URL when you render rather than storing one: it expires at
53
+ `expiresAt`.
54
+
55
+ ## Listing and deleting
56
+
57
+ ```ts
58
+ // list(prefix?) → Promise<BlobObject[]>
59
+ const everything = await canopy.blob.list();
60
+ const avatars = await canopy.blob.list("avatars/");
61
+
62
+ // delete(key) → Promise<void>
63
+ await canopy.blob.delete("notes.txt");
64
+ ```
65
+
66
+ ## Example: upload from a file input
67
+
68
+ ```tsx
69
+ async function onFileChange(event: React.ChangeEvent<HTMLInputElement>) {
70
+ const file = event.target.files?.[0];
71
+ if (!file) return;
72
+
73
+ const stored = await canopy.blob.put(`uploads/${file.name}`, file);
74
+ const { url } = await canopy.blob.getUrl(stored.key);
75
+ setImageUrl(url); // <img src={imageUrl} />
76
+ }
77
+
78
+ <input type="file" accept="image/*" onChange={onFileChange} />;
79
+ ```
80
+
81
+ Storage location and limits are covered in
82
+ [Project data](https://docs.canopy.website/platform/project-data).
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Services
3
+ description: Curated internal data services, invoked through the platform's connector — auth handled for you.
4
+ ---
5
+
6
+ `canopy.services` is the catalog of curated internal data services enabled
7
+ for your project. Calls go through the platform's connector, which handles
8
+ authentication — a service token or on-behalf-of-user credentials, depending
9
+ on the service — so there is nothing to configure in your code.
10
+
11
+ > **Warning:** Services aren't live yet: the SDK surface below is stable, but the platform currently answers
12
+ > every service call with "not implemented" and no services can be enabled. This page describes what
13
+ > to build against once the first services land.
14
+
15
+ ## services.list()
16
+
17
+ The services enabled for this project:
18
+
19
+ ```ts
20
+ const available = await canopy.services.list();
21
+ // [{ name: string, description: string }]
22
+ ```
23
+
24
+ The catalog is curated per project — an empty array means no services are
25
+ enabled yet.
26
+
27
+ ## services.get(name)
28
+
29
+ Returns a `ServiceRef` for a named service. This is a local handle — no
30
+ network call happens until you invoke it:
31
+
32
+ ```ts
33
+ const service = canopy.services.get("example");
34
+ ```
35
+
36
+ ## ref.fetch(path, init?)
37
+
38
+ Invoke a path on the service and get its JSON response back, typed with the
39
+ generic you provide:
40
+
41
+ ```ts
42
+ const pong = await canopy.services.get("example").fetch("/ping");
43
+
44
+ const report = await canopy.services.get("example").fetch<Report>("/reports/weekly", {
45
+ method: "POST",
46
+ body: { week: 33 },
47
+ });
48
+ ```
49
+
50
+ - `path` — the path to invoke on the service, e.g. `"/ping"`.
51
+ - `init.method` — optional HTTP method, defaults to `"GET"`.
52
+ - `init.body` — optional request body, sent as JSON.
53
+ - Returns `Promise<T>` (defaults to `unknown`) — the service's JSON response.
54
+
55
+ Failed invocations throw `CanopyError` with the HTTP `status`.
56
+
57
+ > **Note:** Services are for reading and invoking internal Monks data sources the platform has connected — not
58
+ > arbitrary outbound HTTP. For external APIs, use `fetch` directly.