@polardbx/pxbaas 0.0.0-stage → 0.1.0-beta.1

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 (264) hide show
  1. package/bin/pxbaas.js +13 -0
  2. package/dist/app/preflight.d.ts +38 -0
  3. package/dist/app/preflight.d.ts.map +1 -0
  4. package/dist/app/preflight.js +79 -0
  5. package/dist/app/preflight.js.map +1 -0
  6. package/dist/app/spec-lint.d.ts +45 -0
  7. package/dist/app/spec-lint.d.ts.map +1 -0
  8. package/dist/app/spec-lint.js +407 -0
  9. package/dist/app/spec-lint.js.map +1 -0
  10. package/dist/app/templates.d.ts +143 -0
  11. package/dist/app/templates.d.ts.map +1 -0
  12. package/dist/app/templates.js +151 -0
  13. package/dist/app/templates.js.map +1 -0
  14. package/dist/cli/root.d.ts +3 -0
  15. package/dist/cli/root.d.ts.map +1 -0
  16. package/dist/cli/root.js +69 -0
  17. package/dist/cli/root.js.map +1 -0
  18. package/dist/commands/app/index.d.ts +8 -0
  19. package/dist/commands/app/index.d.ts.map +1 -0
  20. package/dist/commands/app/index.js +1267 -0
  21. package/dist/commands/app/index.js.map +1 -0
  22. package/dist/commands/auth/index.d.ts +3 -0
  23. package/dist/commands/auth/index.d.ts.map +1 -0
  24. package/dist/commands/auth/index.js +198 -0
  25. package/dist/commands/auth/index.js.map +1 -0
  26. package/dist/commands/completion/index.d.ts +3 -0
  27. package/dist/commands/completion/index.d.ts.map +1 -0
  28. package/dist/commands/completion/index.js +10 -0
  29. package/dist/commands/completion/index.js.map +1 -0
  30. package/dist/commands/data/index.d.ts +3 -0
  31. package/dist/commands/data/index.d.ts.map +1 -0
  32. package/dist/commands/data/index.js +245 -0
  33. package/dist/commands/data/index.js.map +1 -0
  34. package/dist/commands/db/index.d.ts +14 -0
  35. package/dist/commands/db/index.d.ts.map +1 -0
  36. package/dist/commands/db/index.js +355 -0
  37. package/dist/commands/db/index.js.map +1 -0
  38. package/dist/commands/doctor/index.d.ts +3 -0
  39. package/dist/commands/doctor/index.d.ts.map +1 -0
  40. package/dist/commands/doctor/index.js +305 -0
  41. package/dist/commands/doctor/index.js.map +1 -0
  42. package/dist/commands/functions/delete.d.ts +3 -0
  43. package/dist/commands/functions/delete.d.ts.map +1 -0
  44. package/dist/commands/functions/delete.js +55 -0
  45. package/dist/commands/functions/delete.js.map +1 -0
  46. package/dist/commands/functions/deploy.d.ts +3 -0
  47. package/dist/commands/functions/deploy.d.ts.map +1 -0
  48. package/dist/commands/functions/deploy.js +118 -0
  49. package/dist/commands/functions/deploy.js.map +1 -0
  50. package/dist/commands/functions/index.d.ts +3 -0
  51. package/dist/commands/functions/index.d.ts.map +1 -0
  52. package/dist/commands/functions/index.js +15 -0
  53. package/dist/commands/functions/index.js.map +1 -0
  54. package/dist/commands/functions/list.d.ts +6 -0
  55. package/dist/commands/functions/list.d.ts.map +1 -0
  56. package/dist/commands/functions/list.js +63 -0
  57. package/dist/commands/functions/list.js.map +1 -0
  58. package/dist/commands/functions/new.d.ts +3 -0
  59. package/dist/commands/functions/new.d.ts.map +1 -0
  60. package/dist/commands/functions/new.js +57 -0
  61. package/dist/commands/functions/new.js.map +1 -0
  62. package/dist/commands/gen/index.d.ts +3 -0
  63. package/dist/commands/gen/index.d.ts.map +1 -0
  64. package/dist/commands/gen/index.js +320 -0
  65. package/dist/commands/gen/index.js.map +1 -0
  66. package/dist/commands/instance/index.d.ts +8 -0
  67. package/dist/commands/instance/index.d.ts.map +1 -0
  68. package/dist/commands/instance/index.js +361 -0
  69. package/dist/commands/instance/index.js.map +1 -0
  70. package/dist/commands/login/index.d.ts +3 -0
  71. package/dist/commands/login/index.d.ts.map +1 -0
  72. package/dist/commands/login/index.js +185 -0
  73. package/dist/commands/login/index.js.map +1 -0
  74. package/dist/commands/login/login.d.ts +3 -0
  75. package/dist/commands/login/login.d.ts.map +1 -0
  76. package/dist/commands/login/login.js +185 -0
  77. package/dist/commands/login/login.js.map +1 -0
  78. package/dist/commands/logout/index.d.ts +3 -0
  79. package/dist/commands/logout/index.d.ts.map +1 -0
  80. package/dist/commands/logout/index.js +41 -0
  81. package/dist/commands/logout/index.js.map +1 -0
  82. package/dist/commands/profile/import-env.d.ts +13 -0
  83. package/dist/commands/profile/import-env.d.ts.map +1 -0
  84. package/dist/commands/profile/import-env.js +81 -0
  85. package/dist/commands/profile/import-env.js.map +1 -0
  86. package/dist/commands/profile/index.d.ts +3 -0
  87. package/dist/commands/profile/index.d.ts.map +1 -0
  88. package/dist/commands/profile/index.js +109 -0
  89. package/dist/commands/profile/index.js.map +1 -0
  90. package/dist/commands/rest/index.d.ts +3 -0
  91. package/dist/commands/rest/index.d.ts.map +1 -0
  92. package/dist/commands/rest/index.js +102 -0
  93. package/dist/commands/rest/index.js.map +1 -0
  94. package/dist/commands/skill/index.d.ts +11 -0
  95. package/dist/commands/skill/index.d.ts.map +1 -0
  96. package/dist/commands/skill/index.js +153 -0
  97. package/dist/commands/skill/index.js.map +1 -0
  98. package/dist/commands/status/index.d.ts +3 -0
  99. package/dist/commands/status/index.d.ts.map +1 -0
  100. package/dist/commands/status/index.js +59 -0
  101. package/dist/commands/status/index.js.map +1 -0
  102. package/dist/commands/storage/index.d.ts +3 -0
  103. package/dist/commands/storage/index.d.ts.map +1 -0
  104. package/dist/commands/storage/index.js +198 -0
  105. package/dist/commands/storage/index.js.map +1 -0
  106. package/dist/commands/whoami/index.d.ts +3 -0
  107. package/dist/commands/whoami/index.d.ts.map +1 -0
  108. package/dist/commands/whoami/index.js +82 -0
  109. package/dist/commands/whoami/index.js.map +1 -0
  110. package/dist/credentials/commit.d.ts +38 -0
  111. package/dist/credentials/commit.d.ts.map +1 -0
  112. package/dist/credentials/commit.js +57 -0
  113. package/dist/credentials/commit.js.map +1 -0
  114. package/dist/credentials/credential-store.d.ts +57 -0
  115. package/dist/credentials/credential-store.d.ts.map +1 -0
  116. package/dist/credentials/credential-store.js +180 -0
  117. package/dist/credentials/credential-store.js.map +1 -0
  118. package/dist/credentials/env-file.d.ts +44 -0
  119. package/dist/credentials/env-file.d.ts.map +1 -0
  120. package/dist/credentials/env-file.js +118 -0
  121. package/dist/credentials/env-file.js.map +1 -0
  122. package/dist/credentials/formatter.d.ts +39 -0
  123. package/dist/credentials/formatter.d.ts.map +1 -0
  124. package/dist/credentials/formatter.js +79 -0
  125. package/dist/credentials/formatter.js.map +1 -0
  126. package/dist/credentials/index.d.ts +11 -0
  127. package/dist/credentials/index.d.ts.map +1 -0
  128. package/dist/credentials/index.js +11 -0
  129. package/dist/credentials/index.js.map +1 -0
  130. package/dist/credentials/jwt-introspect.d.ts +32 -0
  131. package/dist/credentials/jwt-introspect.d.ts.map +1 -0
  132. package/dist/credentials/jwt-introspect.js +64 -0
  133. package/dist/credentials/jwt-introspect.js.map +1 -0
  134. package/dist/credentials/masker.d.ts +20 -0
  135. package/dist/credentials/masker.d.ts.map +1 -0
  136. package/dist/credentials/masker.js +37 -0
  137. package/dist/credentials/masker.js.map +1 -0
  138. package/dist/credentials/profile.d.ts +83 -0
  139. package/dist/credentials/profile.d.ts.map +1 -0
  140. package/dist/credentials/profile.js +184 -0
  141. package/dist/credentials/profile.js.map +1 -0
  142. package/dist/credentials/tenant-guard.d.ts +16 -0
  143. package/dist/credentials/tenant-guard.d.ts.map +1 -0
  144. package/dist/credentials/tenant-guard.js +53 -0
  145. package/dist/credentials/tenant-guard.js.map +1 -0
  146. package/dist/credentials/tty-guard.d.ts +19 -0
  147. package/dist/credentials/tty-guard.d.ts.map +1 -0
  148. package/dist/credentials/tty-guard.js +47 -0
  149. package/dist/credentials/tty-guard.js.map +1 -0
  150. package/dist/db/apply-schema.d.ts +57 -0
  151. package/dist/db/apply-schema.d.ts.map +1 -0
  152. package/dist/db/apply-schema.js +192 -0
  153. package/dist/db/apply-schema.js.map +1 -0
  154. package/dist/db/sql-file.d.ts +49 -0
  155. package/dist/db/sql-file.d.ts.map +1 -0
  156. package/dist/db/sql-file.js +167 -0
  157. package/dist/db/sql-file.js.map +1 -0
  158. package/dist/instance/provision.d.ts +89 -0
  159. package/dist/instance/provision.d.ts.map +1 -0
  160. package/dist/instance/provision.js +139 -0
  161. package/dist/instance/provision.js.map +1 -0
  162. package/dist/instance/zero-store.d.ts +47 -0
  163. package/dist/instance/zero-store.d.ts.map +1 -0
  164. package/dist/instance/zero-store.js +91 -0
  165. package/dist/instance/zero-store.js.map +1 -0
  166. package/dist/main.d.ts +3 -0
  167. package/dist/main.d.ts.map +1 -0
  168. package/dist/main.js +46 -0
  169. package/dist/main.js.map +1 -0
  170. package/dist/skill/assets.d.ts +38 -0
  171. package/dist/skill/assets.d.ts.map +1 -0
  172. package/dist/skill/assets.js +111 -0
  173. package/dist/skill/assets.js.map +1 -0
  174. package/dist/skill/hint.d.ts +39 -0
  175. package/dist/skill/hint.d.ts.map +1 -0
  176. package/dist/skill/hint.js +109 -0
  177. package/dist/skill/hint.js.map +1 -0
  178. package/dist/skill/hosts.d.ts +21 -0
  179. package/dist/skill/hosts.d.ts.map +1 -0
  180. package/dist/skill/hosts.js +38 -0
  181. package/dist/skill/hosts.js.map +1 -0
  182. package/dist/skill/installer.d.ts +88 -0
  183. package/dist/skill/installer.d.ts.map +1 -0
  184. package/dist/skill/installer.js +203 -0
  185. package/dist/skill/installer.js.map +1 -0
  186. package/dist/skill/ownership.d.ts +35 -0
  187. package/dist/skill/ownership.d.ts.map +1 -0
  188. package/dist/skill/ownership.js +52 -0
  189. package/dist/skill/ownership.js.map +1 -0
  190. package/package.json +63 -3
  191. package/skills/pxbaas-app-spec/SKILL.md +163 -0
  192. package/skills/pxbaas-app-spec/references/deploy-options.md +157 -0
  193. package/skills/pxbaas-app-spec/references/guardrails-pitfalls.md +89 -0
  194. package/skills/pxbaas-app-spec/references/runtime-contract.md +151 -0
  195. package/skills/pxbaas-app-spec/references/ssr-session.md +121 -0
  196. package/skills/pxbaas-app-spec/references/templates-examples.md +308 -0
  197. package/skills/pxbaas-cli/SKILL.md +168 -0
  198. package/skills/pxbaas-cli/references/api-reference.md +68 -0
  199. package/skills/pxbaas-cli/references/app-deploy.md +143 -0
  200. package/skills/pxbaas-cli/references/auth-and-credentials.md +88 -0
  201. package/skills/pxbaas-cli/references/data-postgrest.md +85 -0
  202. package/skills/pxbaas-cli/references/db-and-meta.md +85 -0
  203. package/skills/pxbaas-cli/references/edge-functions.md +71 -0
  204. package/skills/pxbaas-cli/references/multi-tenant-apps.md +107 -0
  205. package/skills/pxbaas-cli/references/provision.md +86 -0
  206. package/skills/pxbaas-cli/references/safety-and-two-phase.md +49 -0
  207. package/skills/pxbaas-cli/references/storage.md +61 -0
  208. package/skills/pxbaas-cli/references/troubleshooting.md +73 -0
  209. package/templates/webapp/api/index.ts +55 -0
  210. package/templates/webapp/csr-bff/README.md +81 -0
  211. package/templates/webapp/csr-bff/backend/index.ts +168 -0
  212. package/templates/webapp/csr-bff/db/schema.sql +69 -0
  213. package/templates/webapp/csr-bff/frontend/dist/admin.html +101 -0
  214. package/templates/webapp/csr-bff/frontend/dist/article.html +44 -0
  215. package/templates/webapp/csr-bff/frontend/dist/blog.js +206 -0
  216. package/templates/webapp/csr-bff/frontend/dist/blogger.html +53 -0
  217. package/templates/webapp/csr-bff/frontend/dist/index.html +70 -0
  218. package/templates/webapp/csr-bff/frontend/dist/login.html +56 -0
  219. package/templates/webapp/csr-bff/frontend/dist/profile.html +59 -0
  220. package/templates/webapp/csr-bff/frontend/dist/signup.html +78 -0
  221. package/templates/webapp/csr-bff/frontend/dist/style.css +27 -0
  222. package/templates/webapp/manifest.json +296 -0
  223. package/templates/webapp/mpa/about.html +29 -0
  224. package/templates/webapp/mpa/app.js +42 -0
  225. package/templates/webapp/mpa/index.html +42 -0
  226. package/templates/webapp/mpa/index.ts +60 -0
  227. package/templates/webapp/mpa/style.css +42 -0
  228. package/templates/webapp/react-vite/frontend/index.html +12 -0
  229. package/templates/webapp/react-vite/frontend/package-lock.json +1826 -0
  230. package/templates/webapp/react-vite/frontend/package.json +23 -0
  231. package/templates/webapp/react-vite/frontend/src/App.tsx +34 -0
  232. package/templates/webapp/react-vite/frontend/src/main.tsx +13 -0
  233. package/templates/webapp/react-vite/frontend/src/style.css +46 -0
  234. package/templates/webapp/react-vite/frontend/tsconfig.json +20 -0
  235. package/templates/webapp/react-vite/frontend/vite.config.ts +10 -0
  236. package/templates/webapp/react-vite-hono/backend/index.ts +21 -0
  237. package/templates/webapp/react-vite-hono/frontend/index.html +12 -0
  238. package/templates/webapp/react-vite-hono/frontend/package-lock.json +1826 -0
  239. package/templates/webapp/react-vite-hono/frontend/package.json +23 -0
  240. package/templates/webapp/react-vite-hono/frontend/src/App.tsx +47 -0
  241. package/templates/webapp/react-vite-hono/frontend/src/main.tsx +13 -0
  242. package/templates/webapp/react-vite-hono/frontend/src/style.css +66 -0
  243. package/templates/webapp/react-vite-hono/frontend/tsconfig.json +20 -0
  244. package/templates/webapp/react-vite-hono/frontend/vite.config.ts +10 -0
  245. package/templates/webapp/spa/index.ts +119 -0
  246. package/templates/webapp/ssr-auth/backend/index.ts +133 -0
  247. package/templates/webapp/ssr-auth/frontend/dist/index.html +15 -0
  248. package/templates/webapp/ssr-blog/README.md +111 -0
  249. package/templates/webapp/ssr-blog/backend/index.ts +764 -0
  250. package/templates/webapp/ssr-blog/db/schema.sql +71 -0
  251. package/templates/webapp/ssr-blog/frontend/dist/index.html +39 -0
  252. package/templates/webapp/ssr-frameworks/README.md +64 -0
  253. package/templates/webapp/ssr-frameworks/backend/index.ts +241 -0
  254. package/templates/webapp/ssr-frameworks/frontend/dist/index.html +31 -0
  255. package/templates/webapp/ssr-hono/backend/index.ts +92 -0
  256. package/templates/webapp/ssr-hono/frontend/dist/index.html +12 -0
  257. package/templates/webapp/ssr-shop/README.md +80 -0
  258. package/templates/webapp/ssr-shop/backend/index.ts +491 -0
  259. package/templates/webapp/ssr-shop/db/schema.sql +120 -0
  260. package/templates/webapp/ssr-shop/frontend/dist/index.html +12 -0
  261. package/templates/webapp/vanilla-static/frontend/app.js +6 -0
  262. package/templates/webapp/vanilla-static/frontend/index.html +19 -0
  263. package/templates/webapp/vanilla-static/frontend/style.css +43 -0
  264. package/README.md +0 -3
@@ -0,0 +1,71 @@
1
+ # Edge Functions Management (functions)
2
+
3
+ Covers: `functions new` · `functions deploy` · `functions list` · `functions delete`.
4
+ Backed by the Management API (capability `functions`); deploy/delete need `service_role`.
5
+
6
+ ## Directory convention
7
+
8
+ Functions live in `./functions/<slug>/` with entrypoint `index.ts` by default; override with `--dir` / `--entrypoint`.
9
+ `slug` must match `^[a-z0-9-]+$` (lowercase letters, numbers, hyphens) or the command errors.
10
+
11
+ ## functions new — scaffold (safe, local files only)
12
+
13
+ ```bash
14
+ pxbaas functions new <slug> --dir ./functions --entrypoint index.ts
15
+ ```
16
+
17
+ - **Flags:** `--dir <directory>` (default `./functions`) · `--entrypoint <path>` (default `index.ts`)
18
+ - Generates an entrypoint template under `<dir>/<slug>/`; no remote interaction.
19
+
20
+ ## functions list — list deployed functions (read-only)
21
+
22
+ ```bash
23
+ pxbaas functions list --json
24
+ ```
25
+
26
+ - **Output (`--json`):**
27
+ ```json
28
+ {
29
+ "count": 1,
30
+ "functions": [
31
+ { "slug": "hello", "name": "hello", "status": "ACTIVE", "version": 3,
32
+ "entrypoint_path": "index.ts", "verify_jwt": true,
33
+ "created_at": "2026-09-01T00:00:00Z", "updated_at": "2026-09-15T00:00:00Z" }
34
+ ]
35
+ }
36
+ ```
37
+ - `status` is one of `ACTIVE | INACTIVE | DEPLOYING | ERROR`.
38
+
39
+ ## functions deploy — deploy (write/publish, confirm first)
40
+
41
+ ```bash
42
+ pxbaas functions deploy <slug> --dir ./functions --entrypoint index.ts --json
43
+ # disable JWT verification (not recommended): add --no-verify-jwt
44
+ ```
45
+
46
+ - **Flags:** `--dir` (default `./functions`) · `--entrypoint` (default `index.ts`) · `--no-verify-jwt` · `--json`
47
+ - Pre-checks: function dir exists, entrypoint exists, dir has ≥1 file.
48
+ - Recursively collects files, **skipping** `node_modules`, hidden files/dirs (`.`-prefixed), and `.lock` files.
49
+ - Returns `status / version / entrypoint_path / verify_jwt / updated_at`.
50
+ - ⚠️ **Publish operation, no built-in gate:** restate slug, dir, entrypoint, and verify_jwt to the user and get consent first.
51
+ - ⚠️ `--no-verify-jwt` disables auth on the function (useful for third-party webhooks like Stripe/WeChat, but lowers security) — flag the risk to the user.
52
+ - ⚠️ **Known platform pitfall (multi-tenant deploy):** the Management API deploy is a multipart request whose `metadata` must be a **plain form field without a filename**; if sent as a file part, the server fails to parse it. This is handled in the SDK/driver layer — if deploy returns an odd 400/parse error, see troubleshooting.md and this point.
53
+
54
+ ## functions delete — delete (destructive, confirm first)
55
+
56
+ ```bash
57
+ pxbaas functions delete <slug> --json
58
+ ```
59
+
60
+ - ⚠️ **Irreversible, no built-in gate:** run `functions list` to confirm the slug exists → restate what will be deleted → get explicit consent → then delete.
61
+
62
+ ## Workflow — author → deploy → verify
63
+
64
+ ```bash
65
+ pxbaas functions new hello --json # 1. scaffold (local)
66
+ # 2. edit ./functions/hello/index.ts
67
+ pxbaas functions deploy hello --json # 3. deploy (confirm with user first)
68
+ pxbaas functions list --json # 4. verify it is live (status ACTIVE)
69
+ ```
70
+
71
+ > Invocation endpoint (`/functions/v1/<slug>`) and per-function `verify_jwt` semantics: fetch `${endpoint}/docs` — see api-reference.md.
@@ -0,0 +1,107 @@
1
+ # Multi-Tenant App Deployment & Acceptance
2
+
3
+ Use this when the instance runs `MULTI_TENANT_MODE=true` and you must provision tenants,
4
+ give an app its own tenant database, or prove tenant isolation. Everything below was
5
+ verified against a live multi-tenant instance; the credential split in §1 is the single
6
+ most common source of 401/403 confusion.
7
+
8
+ ## 1. Credential tiers (three-way split — do not mix)
9
+
10
+ | Endpoint family | Credential that works | Rejected credential → error |
11
+ |---|---|---|
12
+ | `/api/platform/*` (Studio: tenants CRUD) | **dashboard Basic Auth** (`DASHBOARD_USERNAME`/`DASHBOARD_PASSWORD`) | global service key → `401 Unauthorized` |
13
+ | `/pg/query` (ad-hoc SQL) | **global service_role key**, sent as BOTH `Authorization: Bearer` and `apikey` | Basic Auth → 401 "apikey or Bearer required"; per-tenant key → `401` |
14
+ | Data plane `/rest/v1`, `/auth/v1`; Management API `/v1/projects/default/*` (apps create/publish, functions deploy) | **per-tenant key** (Management API also needs `X-Tenant-Id`) | global key + `X-Tenant-Id` → `403 Global credentials cannot access tenant resources` |
15
+
16
+ - A per-tenant anon/service key carries a `tenant_id` claim, so **REST and Auth route from the
17
+ key alone** — `X-Tenant-Id` is optional there. Issued user JWTs carry `aud=<tenant_id>`.
18
+ - Using tenant X's key with `X-Tenant-Id: Y` → `403 Token tenant mismatch` (gateway guardrail).
19
+ - CLI: `pxbaas login --endpoint <url> --tenant-id <id>` with the tenant's keys gives a working
20
+ MT profile (`app capabilities`, `functions`, `rest` all fine), but **`pxbaas db query` returns
21
+ 401 in MT** (the pg-meta path does not accept tenant keys). Run tenant SQL through
22
+ `/pg/query` with the global key, using fully-qualified `tenant_<id>.<table>` names.
23
+
24
+ ## 2. Provisioning a tenant
25
+
26
+ ```bash
27
+ curl -sS -X POST -u "$DASHBOARD_USERNAME:$DASHBOARD_PASSWORD" \
28
+ -H 'Content-Type: application/json' \
29
+ -d '{"tenant_id":"acme","plan":"free"}' \
30
+ "$ENDPOINT/api/platform/tenants"
31
+ ```
32
+
33
+ `201` returns the tenant row **including `jwt_secret`, `anon_key`, `service_role_key`,
34
+ `db_name`** — no need to sign JWTs yourself. It also provisions: `CREATE DATABASE
35
+ tenant_<id>`, `CREATE USER` + `GRANT`, a `tenant_db_accounts` row, a private Storage bucket,
36
+ Realtime registration (`realtime_provisioned:false` is non-fatal), and two `_mysqlrest_rls_*`
37
+ tables inside the tenant DB (see §3 — nothing reads them).
38
+
39
+ - `409` → the tenant exists; `GET /api/platform/tenants` (same Basic Auth) returns its keys.
40
+ - `DEFAULT_TENANT_ID` in the instance env is a convention only: `_platform.tenants` may be
41
+ empty and no `default` tenant is implied.
42
+
43
+ ## 3. Data layer & RLS placement (silent-failure trap)
44
+
45
+ | What | Where |
46
+ |---|---|
47
+ | Business tables | the tenant DB: `tenant_<id>.<table>` |
48
+ | RLS metadata (authoritative) | the **central** `supabase._mysqlrest_rls_tables` / `supabase._mysqlrest_rls_policies` |
49
+ | `table_schema` column value | **`tenant_<id>`** (single-tenant instances use `public`) |
50
+ | `_mysqlrest_rls_*` copies inside the tenant DB | created by provisioning, **read by no service** — writing here silently leaves RLS off |
51
+
52
+ MysqlREST loads policies with `schemas = [<tenant db name>]` against the central RLS database
53
+ (`MYSQLREST_RLS_DATABASE`, default `supabase`). A wrong `table_schema` produces **no error and
54
+ no effect**, so always verify behaviourally after registering policies:
55
+
56
+ ```bash
57
+ # anon sees only what the anon policy allows
58
+ curl -sS "$ENDPOINT/rest/v1/posts?select=id,status" -H "apikey: $TENANT_ANON"
59
+ # owner-scoped write with a forged owner must be refused
60
+ curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$ENDPOINT/rest/v1/posts" \
61
+ -H "apikey: $TENANT_ANON" -H "Authorization: Bearer $USER_TOKEN" \
62
+ -H 'Content-Type: application/json' \
63
+ -d '{"title":"x","author_id":"someone-else","status":"draft"}' # expect 403 PGRST302
64
+ ```
65
+
66
+ ## 4. Deploying a webapp into a tenant
67
+
68
+ - Every Management API call needs `Authorization: Bearer <tenant service_role_key>` **and**
69
+ `X-Tenant-Id: <tenant>`.
70
+ - Manifest: `schemaVersion: 2`, and `name` **must equal** the mount's `appName` (mismatch →
71
+ `409 APP_MANIFEST_INVALID`). Candidate slug shape: `app_<mount-uuid>_r_<rev>`; publish with
72
+ `expectedGeneration` (CAS).
73
+ - Upload statics/backend with explicit nested `filename=frontend/dist/<name>` and
74
+ `filename=backend/index.ts` (multipart flattening otherwise loses the nesting).
75
+ - Idempotency: mount create `409` → look the mount up in `_platform.app_mounts` by
76
+ (`app_name`, `tenant_id`) and reuse; publish `409 APP_ARTIFACT_INVALID: Candidate is already
77
+ active` → treat as success.
78
+ - `APP_SSR_PROBE_FAILED` on publish is a **cold-sandbox budget** issue, not an app defect:
79
+ warm that tenant's sandbox (deploy the same worker as a legacy function and hit it until
80
+ 200), then retry publish 3–5×. See pxbaas-app-spec `references/deploy-options.md`.
81
+ - Workers on `/apps` candidates receive **no platform env**, so any baked-in base URL/anon key
82
+ must be the **target instance's** base and the **target tenant's** anon key. Reusing another
83
+ tenant's or another instance's values silently reads the wrong data.
84
+
85
+ ## 5. Cross-tenant acceptance checklist
86
+
87
+ Run all of these before calling a multi-tenant deployment done:
88
+
89
+ | # | Check | Expected |
90
+ |---|---|---|
91
+ | 1 | List business tables per tenant DB | each tenant holds only its own app's tables |
92
+ | 2 | Insert a distinct marker row per tenant, read through each app's public surface | each app shows only its own marker |
93
+ | 3 | Login in tenant X, then send that cookie (explicit `Cookie:` header, bypassing path scoping) to tenant Y's mount | anonymous — the proxy validates the cookie's tenant **and** mount binding, not just `Path` |
94
+ | 4 | Client sends `X-App-User-Id`/`X-App-User-Email` itself | stripped; the worker sees no identity |
95
+ | 5 | Tenant X's user JWT against tenant Y's REST | never returns Y's data (observed: routed by the JWT's own tenant → `404 PGRST200` table not found) |
96
+ | 6 | Tenant X's anon/service key + `X-Tenant-Id: Y` | `403 Token tenant mismatch` |
97
+ | 7 | Two users alternating on the same mount | each always sees their own identity (no shared-jar bleed) |
98
+
99
+ ## 6. Known platform-side issues (report, don't code around them silently)
100
+
101
+ - Publish-time SSR probe has a fixed budget shorter than a cold sandbox's npm module
102
+ resolution (≈60 s with four SSR renderers).
103
+ - `capabilities.ssrLibs` reflects a build-time allow-list and may **under-report** what the
104
+ sandbox can actually resolve — verify by running the worker, not by reading the capability.
105
+ - `APP_MANIFEST_INVALID` does not name the offending manifest field.
106
+ - Tenant DBs contain unread `_mysqlrest_rls_*` copies that invite writing policies to the
107
+ wrong place.
@@ -0,0 +1,86 @@
1
+ # Provisioning & Managing Zero Instances (provision / instance)
2
+
3
+ Covers: `provision` (alias `instance claim`) · `instance list` · `instance release` · `instance credentials` · `instance current`.
4
+
5
+ When a customer has no BaaS instance yet, `pxbaas provision` claims a **disposable, Supabase-compatible instance** from PolarDB-X Zero and binds it to a profile — fully automatic, no human TTY login.
6
+
7
+ ## Why agents MAY run this (unlike `pxbaas login`)
8
+
9
+ `pxbaas login` is human-only because it would require handling user-typed plaintext keys. `pxbaas provision` is different: the keys come from the **first-party Zero endpoint over TLS**, the CLI ingests them straight into the keyring, and **only fingerprints are ever shown**. So an agent can run it end-to-end without ever seeing a plaintext key. Zero instances are short-lived (15 days) and low-sensitivity.
10
+
11
+ ## pxbaas provision — claim an instance
12
+
13
+ ```bash
14
+ pxbaas provision [--name <profile>] [--whitelist-ip <ip,cidr>] [--reuse] [--no-switch] [--force] [--json]
15
+ ```
16
+
17
+ - **Flags:** `--name` (profile to create; default `zero-<id suffix>`) · `--whitelist-ip` (extra allowed IPs; defaults to your egress IP) · `--reuse` (bind an existing active instance instead of claiming) · `--no-switch` (create profile but keep the current one) · `--force` (overwrite a same-named profile) · `--json`
18
+ - **What it does:** `POST https://zero.polardbx.com/api/v1/supabase/instances` → stores anon/service keys in the keyring, writes `~/.baas/zero.json` (token + binding, 0600), creates a `single-tenant` profile, and (by default) switches the current profile to it.
19
+ - **Output (`--json`)** never contains plaintext keys — only `endpoint`, `dashboardUrl`, key **fingerprints**, `expiresAt`, `whitelistIp`:
20
+ ```json
21
+ {
22
+ "status": "provisioned",
23
+ "profile": "zero-d4e5f6",
24
+ "switchedFrom": "default",
25
+ "instance": { "id": "sbx_a1b2...", "endpoint": "http://pxsp-xxx:8000", "dashboardUrl": "http://pxsp-xxx:8000", "expiresAt": "2026-09-05T10:30:00.000Z", "mode": "single-tenant", "whitelistIp": "203.0.113.7" },
26
+ "keys": { "anon": { "fingerprint": "sha256:ab12cd34…wxyz", "role": "anon" }, "serviceRole": { "fingerprint": "sha256:ef56gh78…wxyz", "role": "service_role" } },
27
+ "dashboard": { "username": "supabase", "passwordRevealed": false },
28
+ "nextSteps": ["pxbaas status --json", "pxbaas db tables --json", "pxbaas skill install --host <host>"]
29
+ }
30
+ ```
31
+
32
+ **Gotchas**
33
+ - **15-day TTL, no renewal.** When it expires, data-plane calls fail; claim a fresh one (`pxbaas provision`). `pxbaas doctor` warns as expiry approaches.
34
+ - **Keys are returned ONLY at claim time** (Zero has no credentials re-read endpoint). They are persisted locally; if lost, `release` + `provision` again.
35
+ - **Whitelist IP:** the instance only accepts requests from the whitelisted IP (your egress IP by default). If the machine running `pxbaas` differs from the one that claimed, pass `--whitelist-ip` or requests get rejected at the gateway.
36
+
37
+ ## Multiple instances — how the CLI knows which one
38
+
39
+ **One instance = one profile.** Your own instance (`pxbaas login`) and any Zero instances (`pxbaas provision`) are separate profiles; nothing is overwritten.
40
+
41
+ - The instance a command targets = the **current profile** (`pxbaas profile use <name>`) or an explicit **`--profile <name>`**.
42
+ - Zero profiles carry `source: "zero"` + `zeroInstanceId` + `expiresAt`; `pxbaas profile ls` shows the source and which is current.
43
+ - `pxbaas whoami` / `pxbaas status` / `pxbaas instance current` always tell you which instance you are pointed at.
44
+ - Default profile name is `zero-<id suffix>`; provision refuses to clobber an existing name unless `--force`.
45
+
46
+ ## pxbaas instance list / current
47
+
48
+ ```bash
49
+ pxbaas instance list --json # Zero instances bound locally (reconciled with Zero when a token exists)
50
+ pxbaas instance current --json # the Zero instance behind the current profile (or "not a zero instance")
51
+ ```
52
+
53
+ ## pxbaas instance credentials — reveal dashboard login
54
+
55
+ ```bash
56
+ pxbaas instance credentials <id> [--reveal] [--json]
57
+ ```
58
+
59
+ - Default: endpoint, dashboardUrl, dashboard user (`supabase`), key **fingerprints**, expiry.
60
+ - `--reveal`: additionally prints the **Dashboard password** (for browser login). Handle with care — do not paste into shared logs. The anon/service keys are **never** revealed (they live in the keyring; just use the commands).
61
+
62
+ ## pxbaas instance release — destructive, two-phase
63
+
64
+ ```bash
65
+ pxbaas instance release <id> # preview only
66
+ pxbaas instance release <id> --confirm # actually release (destroys cloud resources)
67
+ pxbaas instance release <id> --confirm --keep-profile # release remotely, keep local profile
68
+ ```
69
+
70
+ - Without `--confirm` it only previews. Release removes the local profile/credentials too (unless `--keep-profile`).
71
+ - If the released instance was the **current** profile, no profile is active afterwards — switch with `pxbaas profile use <name>` or `pxbaas provision`.
72
+
73
+ ## Capacity / rate limits — stop and inform (do NOT loop)
74
+
75
+ Zero is a shared pre-warmed pool with per-caller limits. On these errors, **stop, report, and let the user decide** — never retry in a loop:
76
+
77
+ | Error code | Meaning | Action |
78
+ |---|---|---|
79
+ | `SUPABASE_LIMIT_REACHED` (429) | you hold the max concurrent instances | `pxbaas instance list` → `pxbaas instance release <id> --confirm` |
80
+ | `RATE_LIMIT_EXCEEDED` (429) | too many requests from this IP | wait, do not loop |
81
+ | `SUPABASE_POOL_EXHAUSTED` (503) | pool empty right now | retry ONCE after 1–2 min, else report |
82
+ | `SUPABASE_DISABLED` (503) | service disabled | report; do not retry |
83
+ | `INVALID_WHITELIST_IP` (400) | bad `--whitelist-ip` | use comma-separated IPv4/CIDR |
84
+ | `INSTANCE_NOT_FOUND` (404) | not found / not yours / already released | check `pxbaas instance list` |
85
+
86
+ > After provisioning, the instance also serves its own live API reference at `GET {endpoint}/docs` — see api-reference.md.
@@ -0,0 +1,49 @@
1
+ # Safety Model & Destructive Operations (must read)
2
+
3
+ This is the safety baseline for using `pxbaas`. **The CLI currently has NO built-in confirmation gate**, so the responsibility to confirm falls on YOU (the agent).
4
+
5
+ ## Key fact: two-phase confirmation is NOT implemented
6
+
7
+ - The README/design docs claim destructive operations support two-phase `--plan` / `--confirm <token>`.
8
+ - **In the actual source, this is not implemented.** The descriptions of `db create-table` / `db add-column` / `data delete` say "two-phase confirmation", but there is no `--plan`/`--confirm` flag and no interactive prompt; e.g. `data delete` issues `DELETE /rest/v1/<table>?<filter>` directly.
9
+ - **Implication:** do NOT assume the CLI will block a dangerous operation for you. **You are the confirmation gate.**
10
+
11
+ ## Destructive / write operations (confirm with the user first)
12
+
13
+ | Command | Risk |
14
+ |---|---|
15
+ | `data delete <table> --filter ...` | deletes rows, irreversible |
16
+ | `data insert` / `data update` | writes / mutates data |
17
+ | `db create-table` / `db add-column` | schema change (DDL) |
18
+ | `db query "<write or DDL SQL>"` | depends on the SQL |
19
+ | `functions deploy` | publishes / overwrites a live function |
20
+ | `functions delete <slug>` | deletes a live function, irreversible |
21
+ | `storage rm` / `storage mv` / `storage cp` (overwrite) | deletes / moves / overwrites objects |
22
+ | `auth users delete` / `create` / `invite` | deletes / creates users, sends invites |
23
+ | `rest` POST/PATCH/DELETE/PUT | depends on method and path |
24
+ | `profile rm` / `logout` | removes local credential config |
25
+
26
+ **Read-only safe commands** (no confirmation needed): `whoami` `status` `doctor` `profile ls/use` `db tables/schema` `db query "<SELECT>"` `gen types` `data select` `functions list` `functions new` (local only) `storage ls` `auth users list/get` `completion`.
27
+
28
+ ## Agent confirmation protocol (for destructive ops)
29
+
30
+ ```
31
+ 1) PREVIEW with a read-only command to determine the impact:
32
+ - data delete/update → data select with the same filter
33
+ - storage rm → storage ls with the same prefix
34
+ - functions delete → functions list
35
+ - auth users delete → auth users get
36
+ 2) RESTATE to the user: operation + target (table/function/path/user) + estimated affected rows/objects + reversibility
37
+ 3) WAIT for explicit consent ("yes", "go ahead", "confirm"); ambiguous or no consent → do NOT execute
38
+ 4) EXECUTE the command (with --json)
39
+ 5) VERIFY with a read-only command and report the result
40
+ ```
41
+
42
+ ## Other safety rules
43
+
44
+ - **NEVER run `pxbaas login`** — it needs an interactive TTY; agents cannot complete it. If not logged in, ask the user to log in (see auth-and-credentials.md).
45
+ - **NEVER handle plaintext keys** — output is fingerprint-only (sha256 first 8 + last 4); do not request, echo, or reconstruct a plaintext anon/service key.
46
+ - **Least-privilege role** — prefer `anon` for reads; use `service_role` only when you truly need to bypass RLS, and explain its meaning to the user.
47
+ - **Tenant isolation** — the CLI validates the key's `tenant_id` against the profile; do not mix credentials across profiles or target another tenant's resources.
48
+ - **`--no-verify-jwt`** — `functions deploy --no-verify-jwt` disables JWT verification on the function and lowers security; avoid unless necessary and always tell the user.
49
+ - **Admin scope** — `/docs` lists admin APIs (PG-Meta `/pg/`, Management API `/v1/projects/*`) that the customer CLI does not expose; do not attempt them via `pxbaas`.
@@ -0,0 +1,61 @@
1
+ # Storage Management (storage)
2
+
3
+ Covers: `storage ls` · `storage cp` · `storage mv` · `storage rm`.
4
+ Backed by the Storage API (capability `storage` → `${endpoint}/storage/v1`).
5
+
6
+ ## Path format
7
+
8
+ Always `bucket/path/to/object`: the first segment is the **bucket name**, the rest is the object key (prefix).
9
+
10
+ ## storage ls — list buckets or objects (read-only, safe)
11
+
12
+ ```bash
13
+ pxbaas storage ls --json # no path: list all buckets
14
+ pxbaas storage ls my-bucket --json # list objects in a bucket (top level)
15
+ pxbaas storage ls my-bucket/photos --json # list objects under a prefix
16
+ ```
17
+
18
+ - **Signature:** `storage ls [path]` · **Flags:** `--json`
19
+ - No path → buckets (name / public / created_at). With path → objects in that bucket.
20
+
21
+ ## storage cp — copy/upload/download (write)
22
+
23
+ ```bash
24
+ pxbaas storage cp <src> <dst> [--recursive] --json
25
+ ```
26
+
27
+ - **Flags:** `--recursive` · `--json`
28
+ - Both `src` and `dst` **must include bucket + path** (e.g. `bucket/path/to/file`), otherwise it errors.
29
+ - Supports local↔remote and remote↔remote.
30
+ - ⚠️ write — confirm source and destination with the user (beware silent overwrite).
31
+
32
+ ## storage mv — move/rename (write, remote only)
33
+
34
+ ```bash
35
+ pxbaas storage mv <srcBucket/srcPath> <dstBucket/dstPath> --json
36
+ ```
37
+
38
+ - Moves/renames within remote storage; both ends need bucket + path.
39
+ - ⚠️ changes object location — confirm with the user first.
40
+
41
+ ## storage rm — delete objects (destructive)
42
+
43
+ ```bash
44
+ pxbaas storage rm <bucket/path> [--recursive] --json
45
+ ```
46
+
47
+ - **Flags:** `--recursive` (delete all objects under the prefix) · `--json`
48
+ - ⚠️ **Irreversible, no built-in gate:** run `storage ls <path>` first to confirm the objects/count → restate to the user → get explicit consent → then delete.
49
+ Be especially careful with `--recursive`: a wrong prefix can delete many objects.
50
+
51
+ ## Workflow — safe delete/overwrite
52
+
53
+ ```
54
+ 1) storage ls <target prefix> # preview
55
+ 2) restate to user: bucket + path + object count + operation (rm / mv / cp overwrite)
56
+ 3) get explicit consent
57
+ 4) execute
58
+ 5) storage ls again to verify
59
+ ```
60
+
61
+ > Storage endpoints (public vs authenticated, signed URLs, multi-tenant path rewriting): fetch `${endpoint}/docs` — see api-reference.md.
@@ -0,0 +1,73 @@
1
+ # Troubleshooting (doctor / status + error triage)
2
+
3
+ When the agent hits connection, auth, or permission problems, triage in this order — do not blindly retry or work around.
4
+
5
+ ## Step 1: doctor self-check
6
+
7
+ ```bash
8
+ pxbaas doctor --json
9
+ ```
10
+
11
+ `doctor` runs 5 checks; each returns `status` (pass/warn/fail/skip), `detail`, `duration`, and `remediation` (a fix hint):
12
+
13
+ | Check | Meaning |
14
+ |---|---|
15
+ | **Endpoint Connectivity** | is the endpoint reachable (HTTP) |
16
+ | **JWT Signature** | are the anon/service key JWTs well-formed and valid |
17
+ | **JWT Expiry** | are the JWTs expired |
18
+ | **Tenant Consistency** | in multi-tenant mode, does the key `tenant_id` match the profile (skip in single-tenant) |
19
+ | **Permissions** | are anon / service_role usable |
20
+
21
+ **Output (`--json`):**
22
+ ```json
23
+ {
24
+ "profile": "local",
25
+ "checks": [
26
+ { "name": "Endpoint Connectivity", "status": "pass", "detail": "HTTP 200", "duration": 42 },
27
+ { "name": "JWT Expiry", "status": "warn", "detail": "anon key expires in 3 days", "duration": 1, "remediation": "Re-login before expiry" }
28
+ ],
29
+ "summary": { "pass": 4, "warn": 1, "fail": 0, "skip": 0 }
30
+ }
31
+ ```
32
+
33
+ - Any `fail` → the process exits non-zero. **Read each `remediation` first** and follow it.
34
+ - Do not continue with data-plane commands while a check is failing — fix the root cause first.
35
+
36
+ ## Step 2: confirm capability URLs and identity
37
+
38
+ ```bash
39
+ pxbaas status --json # capability URLs: verify rest/auth/storage/functions/meta addresses
40
+ pxbaas whoami --json # verify profile / tenant / role / key fingerprints
41
+ ```
42
+
43
+ ## Common error triage
44
+
45
+ | Symptom | Likely cause | Action |
46
+ |---|---|---|
47
+ | `No active profile. Use "pxbaas login"` | not logged in / no profile set | **ask the user to run `pxbaas login`** (agents cannot) |
48
+ | `No credentials found for profile: <name>` | no credentials in keyring for that profile | same — ask the user to re-login that profile |
49
+ | **401 Unauthorized** | invalid/expired JWT, missing apikey | check doctor's JWT Signature / Expiry / Permissions; if expired, ask the user to re-login |
50
+ | **403 Forbidden** | insufficient role or RLS denial | confirm whether `service_role` is needed; `data`/`rest` default to anon and are RLS-constrained; do not escalate blindly — explain to the user |
51
+ | **404 Not Found** | table/function/object/bucket does not exist, or wrong capability URL | verify with `db tables` / `functions list` / `storage ls` / `status` |
52
+ | **429 Too Many Requests** | rate limit (esp. Edge Function quotas) | back off and retry slowly; explain the quota to the user; do not hammer |
53
+ | tenant-related failure | key `tenant_id` ≠ profile | check doctor's Tenant Consistency; never mix credentials across profiles |
54
+ | `tenant_suspended` (403) / `tenant_deleted` (410) | tenant not active | report to the user; this is a platform-side state, not fixable via the CLI |
55
+ | `functions deploy` 400 / parse error | multipart `metadata` field format (see edge-functions.md known pitfall) | check whether it is a driver-layer issue; consult the platform Management API |
56
+ | MT: `/api/platform/*` 401 | that Studio surface takes **dashboard Basic Auth**, not the global service key | `-u $DASHBOARD_USERNAME:$DASHBOARD_PASSWORD` (multi-tenant-apps.md §1) |
57
+ | MT: `/pg/query` 401 | it takes the **global** service key as both `Authorization` and `apikey`; Basic Auth and tenant keys are rejected | switch credential tier |
58
+ | MT: `403 Global credentials cannot access tenant resources` | global key used on a tenant-scoped endpoint | use that tenant's own anon/service key |
59
+ | MT: `403 Token tenant mismatch` | the key's `tenant_id` claim ≠ `X-Tenant-Id` | align them; never cross tenants |
60
+ | MT: `db query` 401 on a tenant profile | the pg-meta path rejects tenant keys | run SQL via `/pg/query` + global key with `tenant_<id>.<table>` names |
61
+ | MT: RLS policies registered but have **no effect** | wrong `table_schema` (must be the tenant DB name, not `public`), or written into the tenant DB's unread `_mysqlrest_rls_*` copies | rewrite into the central `supabase._mysqlrest_rls_*`, then verify behaviourally (anon read / forged-owner write → 403) |
62
+ | `app publish` → `APP_MANIFEST_INVALID` | manifest `name` ≠ mount `appName`, or a v1 `mpa` manifest on a schemaVersion-2-only instance | align `name`; migrate the manifest (pxbaas-app-spec `deploy-options.md`) |
63
+ | `app publish` → `APP_SSR_PROBE_FAILED` | **platform-side**: cold tenant sandbox vs the probe budget (heavy `npm:` graph, ≈60 s) | warm the sandbox with a legacy function, then retry publish 3–5× |
64
+ | `app publish` → `APP_ARTIFACT_INVALID: Candidate is already active` | idempotent re-run of the same candidate | treat as success |
65
+
66
+ ## Principles
67
+
68
+ - **Diagnose before acting:** on any failure run `doctor --json` first and read `remediation`.
69
+ - **Never self-rescue auth issues:** for login/credential failures, always ask the user to handle it — do not attempt `login`, do not request plaintext keys.
70
+ - **Report faithfully:** distinguish permission / network / rate-limit / server errors; do not lump 404 as "resource missing" or treat 403 as 401.
71
+ - **Use `--debug`:** reproduce with the global `--debug` flag for more context and share the key output with the user.
72
+
73
+ > Full error-code table and multi-tenant status codes: fetch `${endpoint}/docs` — see api-reference.md.
@@ -0,0 +1,55 @@
1
+ // {{APP_NAME}} — headless JSON backend Edge Function (Deno.serve) with ?action= routing.
2
+ //
3
+ // Spec: follow the `pxbaas-app-spec` skill — `pxbaas app deploy` lints this file against it.
4
+ //
5
+ // Deploy with: pxbaas app deploy {{SLUG}}
6
+ // Base URL: {endpoint}/functions/v1/{{SLUG}}
7
+ // Examples: GET ?action=health -> { ok, now }
8
+ // POST ?action=echo -> echoes the JSON body
9
+ //
10
+ // Guardrails: CORS enabled; JSON-only; no root-absolute asset refs (none here).
11
+ // Extend the switch below with your own actions. Keep secrets out of a
12
+ // verify_jwt=false function (it is publicly reachable).
13
+
14
+ function corsHeaders(): Record<string, string> {
15
+ return {
16
+ "Access-Control-Allow-Origin": "*",
17
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
18
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, X-Tenant-Id",
19
+ };
20
+ }
21
+
22
+ function json(body: unknown, status = 200): Response {
23
+ return new Response(JSON.stringify(body), {
24
+ status,
25
+ headers: { "Content-Type": "application/json", ...corsHeaders() },
26
+ });
27
+ }
28
+
29
+ Deno.serve(async (req: Request) => {
30
+ const url = new URL(req.url);
31
+
32
+ if (req.method === "OPTIONS") {
33
+ return new Response(null, { status: 204, headers: corsHeaders() });
34
+ }
35
+
36
+ const action = url.searchParams.get("action") ?? "health";
37
+
38
+ switch (action) {
39
+ case "health":
40
+ return json({ ok: true, app: "{{APP_NAME}}", now: new Date().toISOString() });
41
+
42
+ case "echo": {
43
+ let body: unknown = null;
44
+ try {
45
+ body = await req.json();
46
+ } catch {
47
+ return json({ ok: false, error: "invalid JSON body" }, 400);
48
+ }
49
+ return json({ ok: true, echo: body });
50
+ }
51
+
52
+ default:
53
+ return json({ ok: false, error: `unknown action: ${action}` }, 400);
54
+ }
55
+ });
@@ -0,0 +1,81 @@
1
+ # {{APP_NAME}} — 前后端分离(CSR + BFF)参考模板
2
+
3
+ **形态**:7 个真实静态页(`index/article/blogger/login/signup/admin/profile`)+ `style.css` + `blog.js`,由 proxy 静态分支直发(**不启 worker**);所有数据/认证走同源 `./api/*` 打到 Deno BFF worker,worker 再转发到实例**内网**的 `/rest/v1`、`/auth/v1`。
4
+ **会话模型**:`proxy-injected` —— 浏览器不直连 Supabase;登录经平台 `./_session/login` 建立 per-mount HttpOnly cookie,proxy 校验后向 worker 注入 `X-App-User-*`,worker 用 `X-App-User-Token` 作为下游 `Authorization`,由 GoTrue/MysqlREST 的 RLS 做最终裁决。
5
+
6
+ ## 为什么 BFF 不能靠"透传浏览器 Authorization"
7
+
8
+ `/apps/{mount}/api/*` 走 `TRUSTED_INTERNAL` 凭据策略,proxy 会剥掉客户端的
9
+ `authorization / cookie / apikey / x-api-key`。实测判据:同一 token 直连公网 `/auth/v1/user` 得 200,
10
+ 经 BFF 透传却得 `403 bad_jwt: invalid claim: missing sub claim` —— 说明中间层剥了头,worker 恒回落 anon。
11
+ 因此本模板的身份来源**只有**注入头:
12
+
13
+ ```ts
14
+ const injected = req.headers.get("x-app-user-token") || ""; // 方案 C
15
+ const userAuth = injected || incomingAuth; // 兼容 /functions 入口的透传
16
+ ```
17
+
18
+ 前端侧对应:登录/注册成功后追加 `fetch("./_session/login")` 建立 cookie,`clearToken()` 时
19
+ `fetch("./_session/logout", { keepalive: true })`(`keepalive` 保证跳转后请求仍能完成);
20
+ 端点不存在时静默忽略,以兼容旧实例与 `/functions` 入口。
21
+
22
+ ## 目录
23
+
24
+ ```
25
+ {{APP_NAME}}/
26
+ ├── app.json # schemaVersion 2;fallback=null(页面都是真实静态文件)
27
+ ├── backend/index.ts # BFF worker:/api/health /api/config /api/me /api/rest/* /api/auth/*
28
+ ├── frontend/dist/ # 7 个页面 + style.css + blog.js(相对路径引用)
29
+ ├── db/schema.sql # 幂等数据层:posts + 5 条 RLS + 3 篇种子
30
+ └── README.md
31
+ ```
32
+
33
+ `fallback: null` 意味着静态未命中即 404(不落到 worker);BFF 只经 `/api/*` 访问,因此不需要 SSR 探针。
34
+
35
+ ## 部署前必须做的一件事:填 DEPLOY CONFIG
36
+
37
+ `backend/index.ts` 顶部两个常量默认为空;留空时除 `/api/health` 外一律返回
38
+ `503 {"error":"deploy config missing"}`(刻意设计:绝不回落到任何真实实例)。
39
+
40
+ ```ts
41
+ const INTERNAL_BASE = ""; // 目标实例内网基址(公网 host 去掉 http./https. 中缀)
42
+ const ANON_KEY = ""; // 单租户=实例 anon;多租户=该租户 anon
43
+ ```
44
+
45
+ worker 只用 anon 作 Kong 的 `apikey`,并把用户 JWT 原样带给下游;**从不使用 service_role**,
46
+ 因此它拿到的权限不会超过调用者本人。
47
+
48
+ ## 数据层
49
+
50
+ ```bash
51
+ sed 's/__SCHEMA__/public/g' db/schema.sql # 单租户
52
+ sed 's/__SCHEMA__/tenant_acme/g' db/schema.sql # 多租户
53
+ ```
54
+
55
+ RLS 元数据写**中心库** `supabase._mysqlrest_rls_*`,`table_schema` = 上面的 `__SCHEMA__`;写错静默失效。
56
+
57
+ ## 部署
58
+
59
+ ```bash
60
+ pxbaas app deploy {{APP_NAME}} --dir . # appName 必须与 app.json 的 name 一致
61
+ ```
62
+
63
+ 走 curl Management API 时:静态文件必须用显式嵌套 `filename=frontend/dist/<name>`,否则路径被压平导致 publish 报缺文件。
64
+
65
+ ## 验收清单
66
+
67
+ | # | 检查 | 期望 |
68
+ |---|---|---|
69
+ | 1 | 7 个页面 + `style.css` + `blog.js` | 全 200,content-type 正确(`text/css` / `text/javascript`) |
70
+ | 2 | `GET /apps/{m}/api/health` | `{"ok":true,"via":"bff"}`(未配置 DEPLOY CONFIG 时也应可用) |
71
+ | 3 | `GET /apps/{m}/api/config` | 返回的 `anon` **等于目标实例/租户的 anon**(证明参数化生效,没串到别的实例) |
72
+ | 4 | `GET /apps/{m}/api/rest/posts` 匿名 | 只见 published |
73
+ | 5 | `POST /apps/{m}/_session/login` | 200 + `Set-Cookie: pxbaas-app-session; Path=/apps/{m}/; HttpOnly` |
74
+ | 6 | **只带 cookie**(不带 Authorization)`GET /api/me` | 返回完整用户对象 |
75
+ | 7 | 带 cookie 发文(`author_id`=自己 / 他人) | 201 / **403 PGRST302** |
76
+ | 8 | 带 cookie 读 posts | published + 自己的 draft |
77
+ | 9 | `POST /api/../_session/logout` 后 `GET /api/me` | `{"user":null}` |
78
+ | 10 | 双用户交替请求 | 各自只见自己(无共享 jar 串味) |
79
+
80
+ > 注意:`logout` 页靠内联 JS 调 `./_session/logout`,**curl 不执行 JS**,用 curl 复验时要直接打 proxy 的退出端点。
81
+ > 另:在容器内以 `Host: 127.0.0.1:8000` 打 `/apps/*` 会得到 `APP_HOST_REJECTED`,属预期;验证请从公网侧发起。