@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.
- package/bin/pxbaas.js +13 -0
- package/dist/app/preflight.d.ts +38 -0
- package/dist/app/preflight.d.ts.map +1 -0
- package/dist/app/preflight.js +79 -0
- package/dist/app/preflight.js.map +1 -0
- package/dist/app/spec-lint.d.ts +45 -0
- package/dist/app/spec-lint.d.ts.map +1 -0
- package/dist/app/spec-lint.js +407 -0
- package/dist/app/spec-lint.js.map +1 -0
- package/dist/app/templates.d.ts +143 -0
- package/dist/app/templates.d.ts.map +1 -0
- package/dist/app/templates.js +151 -0
- package/dist/app/templates.js.map +1 -0
- package/dist/cli/root.d.ts +3 -0
- package/dist/cli/root.d.ts.map +1 -0
- package/dist/cli/root.js +69 -0
- package/dist/cli/root.js.map +1 -0
- package/dist/commands/app/index.d.ts +8 -0
- package/dist/commands/app/index.d.ts.map +1 -0
- package/dist/commands/app/index.js +1267 -0
- package/dist/commands/app/index.js.map +1 -0
- package/dist/commands/auth/index.d.ts +3 -0
- package/dist/commands/auth/index.d.ts.map +1 -0
- package/dist/commands/auth/index.js +198 -0
- package/dist/commands/auth/index.js.map +1 -0
- package/dist/commands/completion/index.d.ts +3 -0
- package/dist/commands/completion/index.d.ts.map +1 -0
- package/dist/commands/completion/index.js +10 -0
- package/dist/commands/completion/index.js.map +1 -0
- package/dist/commands/data/index.d.ts +3 -0
- package/dist/commands/data/index.d.ts.map +1 -0
- package/dist/commands/data/index.js +245 -0
- package/dist/commands/data/index.js.map +1 -0
- package/dist/commands/db/index.d.ts +14 -0
- package/dist/commands/db/index.d.ts.map +1 -0
- package/dist/commands/db/index.js +355 -0
- package/dist/commands/db/index.js.map +1 -0
- package/dist/commands/doctor/index.d.ts +3 -0
- package/dist/commands/doctor/index.d.ts.map +1 -0
- package/dist/commands/doctor/index.js +305 -0
- package/dist/commands/doctor/index.js.map +1 -0
- package/dist/commands/functions/delete.d.ts +3 -0
- package/dist/commands/functions/delete.d.ts.map +1 -0
- package/dist/commands/functions/delete.js +55 -0
- package/dist/commands/functions/delete.js.map +1 -0
- package/dist/commands/functions/deploy.d.ts +3 -0
- package/dist/commands/functions/deploy.d.ts.map +1 -0
- package/dist/commands/functions/deploy.js +118 -0
- package/dist/commands/functions/deploy.js.map +1 -0
- package/dist/commands/functions/index.d.ts +3 -0
- package/dist/commands/functions/index.d.ts.map +1 -0
- package/dist/commands/functions/index.js +15 -0
- package/dist/commands/functions/index.js.map +1 -0
- package/dist/commands/functions/list.d.ts +6 -0
- package/dist/commands/functions/list.d.ts.map +1 -0
- package/dist/commands/functions/list.js +63 -0
- package/dist/commands/functions/list.js.map +1 -0
- package/dist/commands/functions/new.d.ts +3 -0
- package/dist/commands/functions/new.d.ts.map +1 -0
- package/dist/commands/functions/new.js +57 -0
- package/dist/commands/functions/new.js.map +1 -0
- package/dist/commands/gen/index.d.ts +3 -0
- package/dist/commands/gen/index.d.ts.map +1 -0
- package/dist/commands/gen/index.js +320 -0
- package/dist/commands/gen/index.js.map +1 -0
- package/dist/commands/instance/index.d.ts +8 -0
- package/dist/commands/instance/index.d.ts.map +1 -0
- package/dist/commands/instance/index.js +361 -0
- package/dist/commands/instance/index.js.map +1 -0
- package/dist/commands/login/index.d.ts +3 -0
- package/dist/commands/login/index.d.ts.map +1 -0
- package/dist/commands/login/index.js +185 -0
- package/dist/commands/login/index.js.map +1 -0
- package/dist/commands/login/login.d.ts +3 -0
- package/dist/commands/login/login.d.ts.map +1 -0
- package/dist/commands/login/login.js +185 -0
- package/dist/commands/login/login.js.map +1 -0
- package/dist/commands/logout/index.d.ts +3 -0
- package/dist/commands/logout/index.d.ts.map +1 -0
- package/dist/commands/logout/index.js +41 -0
- package/dist/commands/logout/index.js.map +1 -0
- package/dist/commands/profile/import-env.d.ts +13 -0
- package/dist/commands/profile/import-env.d.ts.map +1 -0
- package/dist/commands/profile/import-env.js +81 -0
- package/dist/commands/profile/import-env.js.map +1 -0
- package/dist/commands/profile/index.d.ts +3 -0
- package/dist/commands/profile/index.d.ts.map +1 -0
- package/dist/commands/profile/index.js +109 -0
- package/dist/commands/profile/index.js.map +1 -0
- package/dist/commands/rest/index.d.ts +3 -0
- package/dist/commands/rest/index.d.ts.map +1 -0
- package/dist/commands/rest/index.js +102 -0
- package/dist/commands/rest/index.js.map +1 -0
- package/dist/commands/skill/index.d.ts +11 -0
- package/dist/commands/skill/index.d.ts.map +1 -0
- package/dist/commands/skill/index.js +153 -0
- package/dist/commands/skill/index.js.map +1 -0
- package/dist/commands/status/index.d.ts +3 -0
- package/dist/commands/status/index.d.ts.map +1 -0
- package/dist/commands/status/index.js +59 -0
- package/dist/commands/status/index.js.map +1 -0
- package/dist/commands/storage/index.d.ts +3 -0
- package/dist/commands/storage/index.d.ts.map +1 -0
- package/dist/commands/storage/index.js +198 -0
- package/dist/commands/storage/index.js.map +1 -0
- package/dist/commands/whoami/index.d.ts +3 -0
- package/dist/commands/whoami/index.d.ts.map +1 -0
- package/dist/commands/whoami/index.js +82 -0
- package/dist/commands/whoami/index.js.map +1 -0
- package/dist/credentials/commit.d.ts +38 -0
- package/dist/credentials/commit.d.ts.map +1 -0
- package/dist/credentials/commit.js +57 -0
- package/dist/credentials/commit.js.map +1 -0
- package/dist/credentials/credential-store.d.ts +57 -0
- package/dist/credentials/credential-store.d.ts.map +1 -0
- package/dist/credentials/credential-store.js +180 -0
- package/dist/credentials/credential-store.js.map +1 -0
- package/dist/credentials/env-file.d.ts +44 -0
- package/dist/credentials/env-file.d.ts.map +1 -0
- package/dist/credentials/env-file.js +118 -0
- package/dist/credentials/env-file.js.map +1 -0
- package/dist/credentials/formatter.d.ts +39 -0
- package/dist/credentials/formatter.d.ts.map +1 -0
- package/dist/credentials/formatter.js +79 -0
- package/dist/credentials/formatter.js.map +1 -0
- package/dist/credentials/index.d.ts +11 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +11 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/jwt-introspect.d.ts +32 -0
- package/dist/credentials/jwt-introspect.d.ts.map +1 -0
- package/dist/credentials/jwt-introspect.js +64 -0
- package/dist/credentials/jwt-introspect.js.map +1 -0
- package/dist/credentials/masker.d.ts +20 -0
- package/dist/credentials/masker.d.ts.map +1 -0
- package/dist/credentials/masker.js +37 -0
- package/dist/credentials/masker.js.map +1 -0
- package/dist/credentials/profile.d.ts +83 -0
- package/dist/credentials/profile.d.ts.map +1 -0
- package/dist/credentials/profile.js +184 -0
- package/dist/credentials/profile.js.map +1 -0
- package/dist/credentials/tenant-guard.d.ts +16 -0
- package/dist/credentials/tenant-guard.d.ts.map +1 -0
- package/dist/credentials/tenant-guard.js +53 -0
- package/dist/credentials/tenant-guard.js.map +1 -0
- package/dist/credentials/tty-guard.d.ts +19 -0
- package/dist/credentials/tty-guard.d.ts.map +1 -0
- package/dist/credentials/tty-guard.js +47 -0
- package/dist/credentials/tty-guard.js.map +1 -0
- package/dist/db/apply-schema.d.ts +57 -0
- package/dist/db/apply-schema.d.ts.map +1 -0
- package/dist/db/apply-schema.js +192 -0
- package/dist/db/apply-schema.js.map +1 -0
- package/dist/db/sql-file.d.ts +49 -0
- package/dist/db/sql-file.d.ts.map +1 -0
- package/dist/db/sql-file.js +167 -0
- package/dist/db/sql-file.js.map +1 -0
- package/dist/instance/provision.d.ts +89 -0
- package/dist/instance/provision.d.ts.map +1 -0
- package/dist/instance/provision.js +139 -0
- package/dist/instance/provision.js.map +1 -0
- package/dist/instance/zero-store.d.ts +47 -0
- package/dist/instance/zero-store.d.ts.map +1 -0
- package/dist/instance/zero-store.js +91 -0
- package/dist/instance/zero-store.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +46 -0
- package/dist/main.js.map +1 -0
- package/dist/skill/assets.d.ts +38 -0
- package/dist/skill/assets.d.ts.map +1 -0
- package/dist/skill/assets.js +111 -0
- package/dist/skill/assets.js.map +1 -0
- package/dist/skill/hint.d.ts +39 -0
- package/dist/skill/hint.d.ts.map +1 -0
- package/dist/skill/hint.js +109 -0
- package/dist/skill/hint.js.map +1 -0
- package/dist/skill/hosts.d.ts +21 -0
- package/dist/skill/hosts.d.ts.map +1 -0
- package/dist/skill/hosts.js +38 -0
- package/dist/skill/hosts.js.map +1 -0
- package/dist/skill/installer.d.ts +88 -0
- package/dist/skill/installer.d.ts.map +1 -0
- package/dist/skill/installer.js +203 -0
- package/dist/skill/installer.js.map +1 -0
- package/dist/skill/ownership.d.ts +35 -0
- package/dist/skill/ownership.d.ts.map +1 -0
- package/dist/skill/ownership.js +52 -0
- package/dist/skill/ownership.js.map +1 -0
- package/package.json +63 -3
- package/skills/pxbaas-app-spec/SKILL.md +163 -0
- package/skills/pxbaas-app-spec/references/deploy-options.md +157 -0
- package/skills/pxbaas-app-spec/references/guardrails-pitfalls.md +89 -0
- package/skills/pxbaas-app-spec/references/runtime-contract.md +151 -0
- package/skills/pxbaas-app-spec/references/ssr-session.md +121 -0
- package/skills/pxbaas-app-spec/references/templates-examples.md +308 -0
- package/skills/pxbaas-cli/SKILL.md +168 -0
- package/skills/pxbaas-cli/references/api-reference.md +68 -0
- package/skills/pxbaas-cli/references/app-deploy.md +143 -0
- package/skills/pxbaas-cli/references/auth-and-credentials.md +88 -0
- package/skills/pxbaas-cli/references/data-postgrest.md +85 -0
- package/skills/pxbaas-cli/references/db-and-meta.md +85 -0
- package/skills/pxbaas-cli/references/edge-functions.md +71 -0
- package/skills/pxbaas-cli/references/multi-tenant-apps.md +107 -0
- package/skills/pxbaas-cli/references/provision.md +86 -0
- package/skills/pxbaas-cli/references/safety-and-two-phase.md +49 -0
- package/skills/pxbaas-cli/references/storage.md +61 -0
- package/skills/pxbaas-cli/references/troubleshooting.md +73 -0
- package/templates/webapp/api/index.ts +55 -0
- package/templates/webapp/csr-bff/README.md +81 -0
- package/templates/webapp/csr-bff/backend/index.ts +168 -0
- package/templates/webapp/csr-bff/db/schema.sql +69 -0
- package/templates/webapp/csr-bff/frontend/dist/admin.html +101 -0
- package/templates/webapp/csr-bff/frontend/dist/article.html +44 -0
- package/templates/webapp/csr-bff/frontend/dist/blog.js +206 -0
- package/templates/webapp/csr-bff/frontend/dist/blogger.html +53 -0
- package/templates/webapp/csr-bff/frontend/dist/index.html +70 -0
- package/templates/webapp/csr-bff/frontend/dist/login.html +56 -0
- package/templates/webapp/csr-bff/frontend/dist/profile.html +59 -0
- package/templates/webapp/csr-bff/frontend/dist/signup.html +78 -0
- package/templates/webapp/csr-bff/frontend/dist/style.css +27 -0
- package/templates/webapp/manifest.json +296 -0
- package/templates/webapp/mpa/about.html +29 -0
- package/templates/webapp/mpa/app.js +42 -0
- package/templates/webapp/mpa/index.html +42 -0
- package/templates/webapp/mpa/index.ts +60 -0
- package/templates/webapp/mpa/style.css +42 -0
- package/templates/webapp/react-vite/frontend/index.html +12 -0
- package/templates/webapp/react-vite/frontend/package-lock.json +1826 -0
- package/templates/webapp/react-vite/frontend/package.json +23 -0
- package/templates/webapp/react-vite/frontend/src/App.tsx +34 -0
- package/templates/webapp/react-vite/frontend/src/main.tsx +13 -0
- package/templates/webapp/react-vite/frontend/src/style.css +46 -0
- package/templates/webapp/react-vite/frontend/tsconfig.json +20 -0
- package/templates/webapp/react-vite/frontend/vite.config.ts +10 -0
- package/templates/webapp/react-vite-hono/backend/index.ts +21 -0
- package/templates/webapp/react-vite-hono/frontend/index.html +12 -0
- package/templates/webapp/react-vite-hono/frontend/package-lock.json +1826 -0
- package/templates/webapp/react-vite-hono/frontend/package.json +23 -0
- package/templates/webapp/react-vite-hono/frontend/src/App.tsx +47 -0
- package/templates/webapp/react-vite-hono/frontend/src/main.tsx +13 -0
- package/templates/webapp/react-vite-hono/frontend/src/style.css +66 -0
- package/templates/webapp/react-vite-hono/frontend/tsconfig.json +20 -0
- package/templates/webapp/react-vite-hono/frontend/vite.config.ts +10 -0
- package/templates/webapp/spa/index.ts +119 -0
- package/templates/webapp/ssr-auth/backend/index.ts +133 -0
- package/templates/webapp/ssr-auth/frontend/dist/index.html +15 -0
- package/templates/webapp/ssr-blog/README.md +111 -0
- package/templates/webapp/ssr-blog/backend/index.ts +764 -0
- package/templates/webapp/ssr-blog/db/schema.sql +71 -0
- package/templates/webapp/ssr-blog/frontend/dist/index.html +39 -0
- package/templates/webapp/ssr-frameworks/README.md +64 -0
- package/templates/webapp/ssr-frameworks/backend/index.ts +241 -0
- package/templates/webapp/ssr-frameworks/frontend/dist/index.html +31 -0
- package/templates/webapp/ssr-hono/backend/index.ts +92 -0
- package/templates/webapp/ssr-hono/frontend/dist/index.html +12 -0
- package/templates/webapp/ssr-shop/README.md +80 -0
- package/templates/webapp/ssr-shop/backend/index.ts +491 -0
- package/templates/webapp/ssr-shop/db/schema.sql +120 -0
- package/templates/webapp/ssr-shop/frontend/dist/index.html +12 -0
- package/templates/webapp/vanilla-static/frontend/app.js +6 -0
- package/templates/webapp/vanilla-static/frontend/index.html +19 -0
- package/templates/webapp/vanilla-static/frontend/style.css +43 -0
- package/README.md +0 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Edge Runtime Contract
|
|
2
|
+
|
|
3
|
+
What the BaaS App delivery layer and edge-function sandbox expect of frontend/backend artifacts, and the limits they run under.
|
|
4
|
+
|
|
5
|
+
## Manifest v2 Webapp model
|
|
6
|
+
|
|
7
|
+
A Webapp has a stable App mount backed by an immutable function candidate:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Client → {appEndpoint}/apps/{mount}/ → frontend files
|
|
11
|
+
Client → {appEndpoint}/apps/{mount}/api/* → optional Deno backend (prefix stripped)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The manifest contract is:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"schemaVersion": 2,
|
|
19
|
+
"mode": "webapp",
|
|
20
|
+
"name": "shop",
|
|
21
|
+
"template": "react-vite-hono",
|
|
22
|
+
"frontend": {
|
|
23
|
+
"root": "frontend/dist",
|
|
24
|
+
"routing": "files",
|
|
25
|
+
"index": "index.html",
|
|
26
|
+
"fallback": null
|
|
27
|
+
},
|
|
28
|
+
"backend": {
|
|
29
|
+
"entrypoint": "backend/index.ts",
|
|
30
|
+
"framework": "hono",
|
|
31
|
+
"pathMode": "strip-app-api-prefix",
|
|
32
|
+
"verifyJwt": false,
|
|
33
|
+
"secrets": [],
|
|
34
|
+
"egressPolicy": "deny"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`backend` may be `null`. The CLI writes local deployment state (`mountId`, `candidateFunctionSlug`, `generation`) after upload and publish operations; it excludes that state from the uploaded artifact manifest.
|
|
40
|
+
|
|
41
|
+
Before upload, the CLI requires capability support for schema version 2, `appEndpoint`, and public static delivery. A backend requires native Deno. Hono requires an exact version plus offline readiness. Artifact count and size must stay within the capability response.
|
|
42
|
+
|
|
43
|
+
Publish, rollback, and unpublish use `expectedGeneration` CAS. On conflict, refresh the mount state instead of guessing or blindly retrying.
|
|
44
|
+
|
|
45
|
+
## Legacy Edge Function execution model
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Client
|
|
49
|
+
│ {endpoint}/functions/v1/{slug}
|
|
50
|
+
▼
|
|
51
|
+
Kong gateway (/functions/v1/*, strip_path, prefix not auth-guarded)
|
|
52
|
+
▼
|
|
53
|
+
edge-function-proxy (forwards only when the sandbox is READY; PAUSED → resume; else 503/404)
|
|
54
|
+
▼
|
|
55
|
+
remote sandbox (per tenant) → edge-runtime
|
|
56
|
+
▼
|
|
57
|
+
your user worker: a top-level Deno.serve((req) => Response)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Function body is stored in MySQL (`edge_functions` / `edge_function_files`) and synced into the sandbox; it survives sandbox recycle.
|
|
61
|
+
- The sandbox URL is **internal** (not shareable). Only `{endpoint}/functions/v1/{slug}` (the Kong address) is public.
|
|
62
|
+
|
|
63
|
+
## Backend code contract
|
|
64
|
+
|
|
65
|
+
- **Runtime is Deno**, not Node.js. Do not use `node:fs`, `http`, `process`, `require`, or Node-only npm packages. If you need a package, it must be Deno-compatible (for M1 prefer zero dependencies).
|
|
66
|
+
- **Entry**: a top-level `Deno.serve(handler)`. The runtime creates one user worker per function path and routes requests to it.
|
|
67
|
+
```ts
|
|
68
|
+
Deno.serve((req: Request): Response | Promise<Response> => { ... });
|
|
69
|
+
```
|
|
70
|
+
- **Always return a `Response`** (or a promise of one). Set `Content-Type` explicitly:
|
|
71
|
+
- page → `text/html; charset=utf-8`
|
|
72
|
+
- API → `application/json`
|
|
73
|
+
- **Do not bind a port** or call `listen()`; the runtime hosts the worker and forwards `req`.
|
|
74
|
+
- **Entrypoint filename** must match what you deploy (`index.ts` by convention; the deploy `metadata.entrypoint_path` and `app.json.entrypoint` must agree).
|
|
75
|
+
- **DB access** (optional): `fetch` the instance REST API (`{endpoint}/rest/v1/...` with the anon key). Direct MySQL drivers (e.g. `npm:mysql2`) are **not supported** in the first release — the npm allow-list admits only Hono and the capability `ssrLibs` renderers, and `egressPolicy:"deny"` blocks direct DB connections. Keep credentials server-side inside the function; never inline them into client HTML. (See `/docs` for MySQL-vs-Postgres limitations.)
|
|
76
|
+
|
|
77
|
+
## Resource limits (per worker)
|
|
78
|
+
|
|
79
|
+
| Limit | Value | Consequence if exceeded |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| Memory | ~150 MB | OOM / worker killed |
|
|
82
|
+
| CPU | ~10 s soft / ~20 s hard | request aborted |
|
|
83
|
+
| Wall-clock | ~5 min | request aborted |
|
|
84
|
+
| Sandbox spec | ~1 vCPU / 512 Mi (ACS default) | slow under load |
|
|
85
|
+
| Concurrency | one sandbox shared per tenant; proxy runs a single worker | limited throughput |
|
|
86
|
+
| Functions per tenant | default **500** (`max_edge_functions`) | **429** on deploy beyond quota |
|
|
87
|
+
|
|
88
|
+
> Actual values come from the sandbox image's main service; treat these as the documented defaults. Design demos to stay far below them.
|
|
89
|
+
|
|
90
|
+
## Worker SSR (server-side rendered shells)
|
|
91
|
+
|
|
92
|
+
Apps may declare `frontend.fallback: "worker"` in app.json: static-miss non-`/api` paths then fall through to the Deno worker, which server-renders HTML (SSR-shell model). Rules:
|
|
93
|
+
|
|
94
|
+
- **MUST** declare `ssrProbePath` (safe logical path, e.g. `/page`); publish renders it once and requires 200 + `text/html` + the hydration root marker `<div id="__ssr" data-ssr="v1">` before activation.
|
|
95
|
+
- **MUST** keep the worker single-file self-contained: relative module imports (`./lib/x.ts`) fail at boot on the current runtime (D16). Inline helpers or import platform-pinned packages only (`npm:hono@<pinned>` plus the curated SSR renderers reported by capability `ssrLibs`, each as the exact `npm:<name>@<version>` — a `/subpath` such as `/server` or `/web` is allowed; off-list imports fail spec-lint with `ssr-lib-not-allowed`).
|
|
96
|
+
- **MUST NOT** touch `window`/`document` at import or render time; escape all reflected input server-side.
|
|
97
|
+
- **MUST** make server-rendered relative links **depth-aware**: a page at a second-level path (e.g. `/product/:id`) emits `../x` links while first-level pages emit `./x`; pass the prefix from a per-route base value into nav/forms. Root-absolute paths escape the mount prefix and stay forbidden.
|
|
98
|
+
- **React SSR emits no event handlers**: `renderToString` drops every `on*` prop (lowercase `onclick` included). Wire interactivity with `data-*` attributes (passed through verbatim) plus one client-side delegated listener; verify by grepping the served HTML for the attribute, not by reading component code.
|
|
99
|
+
- **MUST** fetch data client-side after hydration (relative `./api/*` or the instance REST/Auth URLs with the user's JWT) for the default SSR-shell shape. For server-side data fetching (L3 full SSR / SEO first paint), **MUST** use the platform-injected `SUPABASE_INTERNAL_URL` as the base — it is the ONLY origin reachable from inside the sandbox (the public `SUPABASE_URL` is egress-filtered and cluster service names do not resolve). The platform derives `SUPABASE_INTERNAL_URL` from the public host via DX-12 (edge-runtime `app-env.ts`, MR #30497903); the worker just reads it and **MUST NOT** re-derive from `SUPABASE_URL` or fetch `SUPABASE_URL` directly. Use `SUPABASE_ANON_KEY` (or the forwarded user JWT) as the credential; server-side RLS still applies on the internal base. These three public keys reach App candidate workers only on sandbox images carrying MR #30497903 — degrade gracefully (render the shell without server data) when `SUPABASE_INTERNAL_URL` is empty.
|
|
100
|
+
- SSR responses are `private, no-cache` with the platform CSP (inline hydrate scripts allowed); do not rely on shared caching of rendered pages.
|
|
101
|
+
- **Path shapes (interim contract while `APP_WORKER_PATH_REWRITE` is off)**: the `/api/*` branch delivers the STRIPPED logical path to the worker (`/apps/{m}/api/data` → worker sees `/data`), while static-miss fallthrough delivers the FULL logical path (`/apps/{m}/page` → worker sees `/page`). Workers MUST register both the stripped and `/api`-prefixed shapes for every JSON endpoint (the `ssr-hono` template does); hydrate scripts calling `./api/*` from an SSR page rely on the stripped shape being routed.
|
|
102
|
+
- Start from the `ssr-hono` template (`pxbaas app new --template ssr-hono`); spec-lint enforces the rules above (`ssr-probe-declared`, `ssr-self-contained`, `ssr-marker-present`, `ssr-no-browser-globals`, `ssr-lib-not-allowed`).
|
|
103
|
+
|
|
104
|
+
## Lifecycle & cold start
|
|
105
|
+
|
|
106
|
+
- Sandbox lifecycle mode is `pause`: after idle (`SANDBOX_TIMEOUT` ~3600s) the sandbox pauses.
|
|
107
|
+
- **First request after pause** triggers a resume (~10s) or, if fully recycled, a create (health-poll up to ~60s). During non-READY/non-PAUSED states the proxy returns **503** (404 if the function does not exist).
|
|
108
|
+
- Implication: a freshly shared demo URL may **503 / blank on first hit**. Always verify with retries (see deploy-options.md) and consider a warm-up ping.
|
|
109
|
+
|
|
110
|
+
## Static assets
|
|
111
|
+
|
|
112
|
+
Manifest v2 Webapp frontend rules:
|
|
113
|
+
|
|
114
|
+
- Stage only prebuilt output under `frontend.root`; React/Vite source and dependencies are not deployment artifacts.
|
|
115
|
+
- Vite must use `base: "./"` and disable source maps.
|
|
116
|
+
- React must use `HashRouter`; `BrowserRouter` requires unsupported server fallback routing.
|
|
117
|
+
- Do not include `.env*`, `*.map`, `node_modules/`, `src/`, Service Workers, root-absolute URLs, or binary assets.
|
|
118
|
+
- `frontend.routing` is `"files"`, `index` is `"index.html"`, and the first release requires `fallback: null`.
|
|
119
|
+
|
|
120
|
+
Legacy modes have two supported shapes:
|
|
121
|
+
|
|
122
|
+
- **Single-file (spa/api, default).** All HTML/CSS/JS is **inlined** in one self-contained `index.ts`. This is the simplest, most portable shape for a shareable demo. Never reference separate asset files by root-absolute path (they escape the `/functions/v1/` prefix).
|
|
123
|
+
- **Multi-page (mpa).** Deploy real static files (`index.html`, `about.html`, `style.css`, `app.js`, ...) and declare `"mode": "mpa"` in `app.json`. The BaaS **proxy static branch** serves those files directly by path (`GET /functions/v1/{slug}/style.css` → `text/css`), with correct MIME, an `ETag` (the function version) and `Cache-Control`; the Deno worker only handles the JSON API (`?action=...`) and non-asset paths. Pages must link assets with **relative** URLs (`style.css`, `./app.js`, `about.html`) so they resolve under the `/functions/v1/{slug}/` prefix.
|
|
124
|
+
|
|
125
|
+
Static-branch facts (mpa):
|
|
126
|
+
|
|
127
|
+
- **Mode-gated.** The branch activates only when the deployed `app.json` has `mode=="mpa"`. Any other mode (or no `app.json`) routes everything to the worker exactly as before — spa/api behaviour is unchanged.
|
|
128
|
+
- **GET/HEAD only.** POST/PUT/DELETE/PATCH and CORS-preflight OPTIONS fall through to the worker; only GET/HEAD are served statically.
|
|
129
|
+
- **Text-only.** Served extensions are text (`.html/.htm/.css/.js/.mjs/.svg/.json/.map/.txt`). **Binary assets (png/jpg/gif/webp/ico/woff/woff2/ttf/…) are NOT served** — the utf-8 upload pipeline corrupts them, so `pxbaas app deploy` blocks them (`no-binary-assets`); host them on **Storage/OSS** and reference by URL.
|
|
130
|
+
- **Nested paths supported.** Files may live in subdirectories (`assets/style.css`, `pages/about.html`); the CLI preserves the relative path on upload. Reference them relatively from the pages.
|
|
131
|
+
- **Miss falls through.** A path that is not a stored static file is forwarded to the worker (SPA fallback / custom 404), never hard-404'd by the branch. The manifest `app.json` itself is never served.
|
|
132
|
+
- **Caching.** HTML is served `Cache-Control: no-cache` (revalidate every hit → a redeploy shows immediately; a 304 is cheap). Other text assets get `public, max-age=<STATIC_CACHE_TTL>` with `ETag`=function version, so a redeploy invalidates them.
|
|
133
|
+
- **Per-asset cap.** Files larger than `STATIC_MAX_BYTES` (default 256 KB) fall through to the worker; host large binaries on **Storage/OSS**.
|
|
134
|
+
- **Rollback.** `STATIC_SERVING_ENABLED=false` disables the branch entirely.
|
|
135
|
+
|
|
136
|
+
For large frontends regardless of mode, host heavy static assets on Storage/CDN instead of inside the function.
|
|
137
|
+
|
|
138
|
+
## Webapp backend restrictions
|
|
139
|
+
|
|
140
|
+
- The backend entrypoint must call `Deno.serve`; do not bind a port.
|
|
141
|
+
- Node APIs (`node:*`, `require`, `process`, and Node server APIs) are forbidden.
|
|
142
|
+
- Runtime `Deno.env` access is limited to the platform public three-key whitelist `SUPABASE_URL` / `SUPABASE_ANON_KEY` / `SUPABASE_INTERNAL_URL` (injected into App candidate workers per edge-runtime `app-env.ts`, MR #30497903). Reading any other variable or `Deno.env.toObject()`, non-empty Secrets, and any egress policy other than `deny` are unsupported in the first release (`deno-env-not-allowed`, `custom-secrets-not-supported`). `SUPABASE_URL` is render-only — never fetch through it; use `SUPABASE_INTERNAL_URL` as the data-plane base.
|
|
143
|
+
- `egressPolicy: "deny"` means **no public egress**, not "no network": the sandbox egress filter blocks public origins and arbitrary outbound, but the platform-internal base (`SUPABASE_INTERNAL_URL`) stays reachable, so the worker MAY fetch the instance's own REST/Auth/Storage through it. Only `deny` is accepted in the first release (any other value is a blocker).
|
|
144
|
+
- Arbitrary `http:`, `https:`, and `jsr:` imports are blocked. The only approved `npm:` packages are Hono (exact capability version) plus the curated SSR renderers reported by capability `ssrLibs` (exact `npm:<name>@<version>`, optional `/subpath`); anything else fails spec-lint (`ssr-lib-not-allowed`, or `no-remote-import` when the instance reports no `ssrLibs`).
|
|
145
|
+
- Scaffolded Hono source uses `npm:hono@{{HONO_VERSION}}`; candidate staging substitutes the exact version without modifying local source.
|
|
146
|
+
|
|
147
|
+
## What the runtime is NOT
|
|
148
|
+
|
|
149
|
+
- Not a general app server (no Express/Koa/Next SSR server process).
|
|
150
|
+
- Not a server-fallback SPA host. Manifest v2 uses file routing with `fallback: null`, so client-side apps must use hash routing. Legacy `mpa` serves exact static files but has no rewrite engine.
|
|
151
|
+
- Not a place for long jobs, heavy builds, or large binaries.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SSR Webapp Sessions (Plan C: proxy-injected identity headers)
|
|
2
|
+
|
|
3
|
+
How a Manifest-v2 Webapp gets a **login session** while keeping the worker a pure
|
|
4
|
+
server-side renderer. Read this whenever a webapp needs "signed-in SSR pages" on
|
|
5
|
+
the `/apps/{mount}/` entry.
|
|
6
|
+
|
|
7
|
+
## Why cookies do not work on `/apps/{mount}/`
|
|
8
|
+
|
|
9
|
+
- The app-api dispatch uses `CredentialPolicy.TRUSTED_INTERNAL`: the proxy strips
|
|
10
|
+
`cookie`, `authorization`, `apikey`, `x-api-key` (and client `x-app-*`) before
|
|
11
|
+
forwarding to the worker. A worker can therefore never see a browser cookie on
|
|
12
|
+
this entry (`capabilities.persistentSession` used to be `false` for this reason).
|
|
13
|
+
- The proxy's shared forwarding client historically replayed worker-issued
|
|
14
|
+
`Set-Cookie` across requests (session bleed), and swallows `Set-Cookie` on 3xx.
|
|
15
|
+
Do not build a worker-side cookie session on `/apps`.
|
|
16
|
+
|
|
17
|
+
## The capability (Plan C)
|
|
18
|
+
|
|
19
|
+
When the instance enables session injection, the proxy:
|
|
20
|
+
|
|
21
|
+
1. validates a **per-mount scoped** session cookie
|
|
22
|
+
(`__Host-pxbaas-app`, `HttpOnly`, `SameSite=Lax`, `Path=/apps/{mount}/`), and
|
|
23
|
+
2. injects the authenticated identity as **trusted headers** to the worker.
|
|
24
|
+
|
|
25
|
+
Discover it first:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pxbaas app capabilities --json
|
|
29
|
+
# sessionMode: "proxy-injected-header" | "none"
|
|
30
|
+
# sessionHeaders: ["X-App-User-Id","X-App-User-Token","X-App-User-Email","X-App-Session-Gen"]
|
|
31
|
+
# sessionCookie: { name, scope:"per-mount", httponly:true, sameSite:"Lax" }
|
|
32
|
+
# persistentSession: true when sessionMode != "none"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`capabilities.sessionHeaders` is the **only** source of truth for header names.
|
|
36
|
+
|
|
37
|
+
## How to write the worker
|
|
38
|
+
|
|
39
|
+
- Read identity ONLY from the injected headers:
|
|
40
|
+
`req.headers.get("x-app-user-token")`, `...("x-app-user-id")`, `...("x-app-user-email")`.
|
|
41
|
+
- **Absent headers = anonymous.** Render the anonymous page / a sign-in prompt; do
|
|
42
|
+
not error. This also makes the same code work on instances without the
|
|
43
|
+
capability and on the `/functions/v1/{slug}` entry (which has no injected headers).
|
|
44
|
+
- Use `X-App-User-Token` as `Authorization: Bearer` when calling `/rest/v1` or
|
|
45
|
+
`/auth/v1` server-side; RLS enforces per-user visibility. Do not verify JWTs in
|
|
46
|
+
the worker (the proxy already validated the session).
|
|
47
|
+
- **Never** read the `cookie` header, **never** send `Set-Cookie`, **never** put the
|
|
48
|
+
token in a URL (`?sid=`, `&token=`). spec-lint blocks these for
|
|
49
|
+
`backend.session="proxy-injected"`.
|
|
50
|
+
|
|
51
|
+
## spec-lint session rules (quick reference)
|
|
52
|
+
|
|
53
|
+
Enforced by `pxbaas app deploy` only when `backend.session` is declared:
|
|
54
|
+
|
|
55
|
+
| rule id | severity | fires when | fix |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `session-no-cookie-read` | blocker | proxy-injected worker reads the `cookie` header | read the injected `X-App-User-*` headers |
|
|
58
|
+
| `session-no-set-cookie` | blocker | proxy-injected worker issues `Set-Cookie` | the proxy owns the session cookie |
|
|
59
|
+
| `session-no-url-token` | blocker | user token carried in a URL (`?sid=`, `&access_token=`, `&session_token=`) | use the injected `X-App-User-Token` |
|
|
60
|
+
| `session-header-name-whitelist` | blocker | references an `X-App-User-*` header not in `capabilities.sessionHeaders` | use only the capability-listed header names |
|
|
61
|
+
| `session-use-injected-header` | warning | proxy-injected declared but no `sessionHeaders` name is referenced | read `X-App-User-Token`/`X-App-User-Id` |
|
|
62
|
+
| `session-env-secret` | warning | worker references `JWT_SECRET` | the proxy verifies the session; the worker needs no signing secret |
|
|
63
|
+
| `session-browser-side-no-worker-auth` | warning | `session:"browser-side"` but the worker reads `cookie`/`authorization` | do auth in the browser (direct `/auth/v1`, `/rest/v1`) |
|
|
64
|
+
|
|
65
|
+
## Anti-patterns (never do these in an `/apps` worker)
|
|
66
|
+
|
|
67
|
+
- Reading the `cookie` header, or issuing `Set-Cookie` (the proxy strips/manages both).
|
|
68
|
+
- Putting a session token in a URL (`?sid=`, `&token=`) — it leaks via logs and referrers.
|
|
69
|
+
- Using `localStorage`/browser storage inside the worker (there is no browser on the server).
|
|
70
|
+
- Inventing `X-App-User-*` header names that are not in `capabilities.sessionHeaders`.
|
|
71
|
+
- Verifying JWTs in the worker or reading `JWT_SECRET` (the proxy already validated the session).
|
|
72
|
+
|
|
73
|
+
## Login / logout endpoints (proxy-owned)
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
POST {app-url}/_session/login body {"email","password"} -> Set-Cookie (per-mount)
|
|
77
|
+
POST {app-url}/_session/logout -> clears the cookie
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The proxy authenticates against the mount's tenant GoTrue and sets/clears the
|
|
81
|
+
scoped cookie. Workers do not implement login.
|
|
82
|
+
|
|
83
|
+
## manifest
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
"backend": { "entrypoint": "backend/index.ts", "framework": "deno",
|
|
87
|
+
"pathMode": "strip-app-api-prefix", "verifyJwt": false,
|
|
88
|
+
"secrets": [], "egressPolicy": "deny", "session": "proxy-injected" }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`session` enum: `"none"` (default) | `"proxy-injected"` | `"browser-side"`.
|
|
92
|
+
Deploying `session:"proxy-injected"` to an instance whose
|
|
93
|
+
`capabilities.sessionMode != "proxy-injected-header"` is rejected
|
|
94
|
+
(`APP_CAPABILITY_UNSUPPORTED`) by both the CLI preflight and Studio publish.
|
|
95
|
+
|
|
96
|
+
## Choosing a session model (decision tree)
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
capabilities.sessionMode == "proxy-injected-header"
|
|
100
|
+
-> session:"proxy-injected": pure SSR with signed-in pages (preferred).
|
|
101
|
+
else
|
|
102
|
+
-> session:"browser-side": SSR shell + browser-side auth (localStorage token,
|
|
103
|
+
browser calls /auth/v1 and /rest/v1 directly). Immune to cookie stripping.
|
|
104
|
+
-> or session:"none": public read-only SSR.
|
|
105
|
+
-> legacy only: /functions/v1/{slug} cookie session, or ?sid URL token (demo-grade).
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Multi-tenant
|
|
109
|
+
|
|
110
|
+
The session cookie is bound to `(tenant_id, mount_id)`; the proxy rejects a cookie
|
|
111
|
+
whose tenant does not match the mount's tenant, and injects a tenant-scoped user
|
|
112
|
+
token. Workers need no tenant handling (`X-Tenant-Id` is proxy-injected as before).
|
|
113
|
+
|
|
114
|
+
## Start from the template
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pxbaas app new <name> --template ssr-auth
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It pre-wires the injected-header reads, the anonymous fallback, and the
|
|
121
|
+
proxy-owned login/logout form actions.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Templates, App Structure & Full Examples
|
|
2
|
+
|
|
3
|
+
Canonical shapes for legacy Edge Function apps and schemaVersion 2 Webapps. `pxbaas app new --template <id>` renders nested template files with `{{APP_NAME}}`/`{{SLUG}}` substituted. Hono's `{{HONO_VERSION}}` placeholder is intentionally preserved until candidate staging.
|
|
4
|
+
|
|
5
|
+
## App directory structure
|
|
6
|
+
|
|
7
|
+
Manifest v2 Webapp:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
apps/<name>/
|
|
11
|
+
├── frontend/
|
|
12
|
+
│ ├── package.json # React templates only; exact versions
|
|
13
|
+
│ ├── package-lock.json # committed reproducible lockfile
|
|
14
|
+
│ ├── vite.config.ts # base: "./", sourcemap: false
|
|
15
|
+
│ ├── src/ # React + HashRouter + ordinary CSS
|
|
16
|
+
│ └── dist/ # build output deployed by the CLI
|
|
17
|
+
├── backend/index.ts # react-vite-hono only
|
|
18
|
+
└── app.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`vanilla-static` stores editable files directly under `frontend/`; deploy maps them to canonical `frontend/dist/*`. React templates require `npm ci && npm run build` before deploy.
|
|
22
|
+
|
|
23
|
+
Legacy single-file (`spa`/`api`):
|
|
24
|
+
```
|
|
25
|
+
apps/<name>/
|
|
26
|
+
├── index.ts # entrypoint — the whole webapp (Deno edge function)
|
|
27
|
+
└── app.json # manifest (written by `pxbaas app new`)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Multi-page (`mpa`) — real static files served by path:
|
|
31
|
+
```
|
|
32
|
+
apps/<name>/
|
|
33
|
+
├── index.ts # worker: JSON API (?action=) + non-asset fallback only
|
|
34
|
+
├── index.html # static home page → GET /functions/v1/<slug>/
|
|
35
|
+
├── about.html # static second page → GET /functions/v1/<slug>/about.html
|
|
36
|
+
├── style.css # static asset → GET /functions/v1/<slug>/style.css
|
|
37
|
+
├── app.js # static asset (browser)→ GET /functions/v1/<slug>/app.js
|
|
38
|
+
└── app.json # manifest with mode="mpa" + pages/assets
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`app.json` (spa/api):
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"name": "shop",
|
|
45
|
+
"slug": "shop",
|
|
46
|
+
"template": "spa",
|
|
47
|
+
"entrypoint": "index.ts",
|
|
48
|
+
"verifyJwt": false,
|
|
49
|
+
"createdAt": "2026-09-16T00:00:00.000Z",
|
|
50
|
+
"mode": "spa"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`app.json` (mpa — `mode`/`pages`/`assets` drive the proxy static branch and spec-lint):
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"name": "site",
|
|
58
|
+
"slug": "site",
|
|
59
|
+
"template": "mpa",
|
|
60
|
+
"entrypoint": "index.ts",
|
|
61
|
+
"verifyJwt": false,
|
|
62
|
+
"createdAt": "2026-09-16T00:00:00.000Z",
|
|
63
|
+
"mode": "mpa",
|
|
64
|
+
"pages": ["index.html", "about.html"],
|
|
65
|
+
"assets": ["style.css", "app.js"]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
`pxbaas app deploy/verify/url/remove` read `slug`/`entrypoint`/`verifyJwt`/`template`/`mode`/`pages`/`assets` from this file (CLI flags override). slug must match `^[a-zA-Z0-9][a-zA-Z0-9_-]*$` (≤128). **`app.json` is uploaded with the deploy** so the proxy can read `mode` back from `edge_function_files`.
|
|
69
|
+
|
|
70
|
+
## Templates
|
|
71
|
+
|
|
72
|
+
| id | shape | use for |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `spa` | single page (inline HTML/CSS/JS) + built-in `?action=` JSON API | legacy shareable demos with a UI |
|
|
75
|
+
| `api` | headless JSON backend, `?action=` routing + CORS | legacy webhooks / APIs |
|
|
76
|
+
| `mpa` | real static files + worker `?action=` API | legacy multi-page sites |
|
|
77
|
+
| `vanilla-static` | plain HTML/CSS/JS frontend, no backend | minimal Manifest v2 static app |
|
|
78
|
+
| `react-vite` | React 18.3.1 + React DOM 18.3.1 + React Router DOM 6.28.0 + Vite 5.4.21 | Manifest v2 React frontend |
|
|
79
|
+
| `react-vite-hono` | same React frontend + Hono backend | Manifest v2 full-stack app |
|
|
80
|
+
| `ssr-hono` | Hono worker server-renders the page shell at `/page`; browser hydrates and fetches data | SSR-shell + client data fetching |
|
|
81
|
+
| `ssr-auth` | worker reads the proxy-injected `X-App-User-*` headers and renders authenticated pages | SSR with `session: "proxy-injected"` |
|
|
82
|
+
| `ssr-blog` | full-site SSR blog, zero npm deps, path routes (`/posts`, `/post/:id`), `session: "proxy-injected"`, ships `db/schema.sql` | the reference SSR + platform-session app |
|
|
83
|
+
| `csr-bff` | real static pages + same-origin Deno BFF worker proxying `/api/*` to internal REST/Auth, `fallback: null` | the reference front/back separated (CSR) app |
|
|
84
|
+
| `ssr-shop` | React 18 + Hono SSR shop; catalog/detail/search server-rendered, cart/checkout hydrate; `session: "browser-side"`, RLS-gated orders | framework SSR with browser-side auth |
|
|
85
|
+
| `ssr-frameworks` | same UI rendered by React / Vue / Preact / Solid on Hono, plus `/compare`; no data plane | framework SSR capability showcase (heaviest cold start) |
|
|
86
|
+
|
|
87
|
+
`ssr-blog`, `csr-bff` and `ssr-shop` each ship a `db/schema.sql` (idempotent DDL + RLS + seed) with a `__SCHEMA__`
|
|
88
|
+
token: use `public` on single-tenant instances and `tenant_<id>` on multi-tenant ones. Their workers carry a
|
|
89
|
+
**DEPLOY CONFIG** block (`INTERNAL_BASE` / `ANON_KEY`) that is empty by design — an unconfigured worker returns an
|
|
90
|
+
explicit 503 instead of silently falling back to someone else's instance or tenant.
|
|
91
|
+
|
|
92
|
+
List them at runtime: `pxbaas app templates --json`.
|
|
93
|
+
|
|
94
|
+
## Manifest v2 example
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"schemaVersion": 2,
|
|
99
|
+
"mode": "webapp",
|
|
100
|
+
"name": "shop",
|
|
101
|
+
"template": "react-vite-hono",
|
|
102
|
+
"frontend": {
|
|
103
|
+
"root": "frontend/dist",
|
|
104
|
+
"routing": "files",
|
|
105
|
+
"index": "index.html",
|
|
106
|
+
"fallback": null
|
|
107
|
+
},
|
|
108
|
+
"backend": {
|
|
109
|
+
"entrypoint": "backend/index.ts",
|
|
110
|
+
"framework": "hono",
|
|
111
|
+
"pathMode": "strip-app-api-prefix",
|
|
112
|
+
"verifyJwt": false,
|
|
113
|
+
"secrets": [],
|
|
114
|
+
"egressPolicy": "deny"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The React entrypoint must wrap the app in `HashRouter`. Client API calls use relative `./api/...` URLs. Hono source imports `npm:hono@{{HONO_VERSION}}`; never replace the placeholder manually or use `latest`/ranges. The CLI resolves the capability's exact version in the uploaded candidate.
|
|
120
|
+
|
|
121
|
+
## Full example — `spa` (page + API in one file)
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
function cors(): Record<string, string> {
|
|
125
|
+
return {
|
|
126
|
+
"Access-Control-Allow-Origin": "*",
|
|
127
|
+
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
|
|
128
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, X-Tenant-Id",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function esc(s: string): string {
|
|
132
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pageHtml(serverIso: string): string {
|
|
136
|
+
return `<!DOCTYPE html>
|
|
137
|
+
<html lang="en"><head>
|
|
138
|
+
<meta charset="utf-8" />
|
|
139
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
140
|
+
<title>Demo</title>
|
|
141
|
+
<style>
|
|
142
|
+
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 40px auto; padding: 0 20px; line-height: 1.6; }
|
|
143
|
+
.card { border: 1px solid #ddd; border-radius: 12px; padding: 16px 20px; margin: 16px 0; }
|
|
144
|
+
code, pre { background: #f4f4f5; padding: 2px 6px; border-radius: 4px; word-break: break-all; }
|
|
145
|
+
button { padding: 8px 16px; border-radius: 8px; border: 1px solid #ccc; cursor: pointer; }
|
|
146
|
+
</style></head>
|
|
147
|
+
<body>
|
|
148
|
+
<h1>Demo</h1>
|
|
149
|
+
<div class="card"><div>Share URL</div><code id="url">loading…</code>
|
|
150
|
+
<div>Server time (SSR): ${esc(serverIso)}</div></div>
|
|
151
|
+
<div class="card"><div>Counter</div><span id="count" style="font-size:32px">0</span>
|
|
152
|
+
<button onclick="inc()">+1</button></div>
|
|
153
|
+
<div class="card"><button onclick="ping()">GET ?action=time</button><pre id="api"></pre></div>
|
|
154
|
+
<script>
|
|
155
|
+
document.getElementById("url").textContent = location.origin + location.pathname;
|
|
156
|
+
let n = 0; function inc(){ n++; document.getElementById("count").textContent = n; }
|
|
157
|
+
async function ping(){
|
|
158
|
+
const el = document.getElementById("api"); el.textContent = "loading…";
|
|
159
|
+
const r = await fetch(location.pathname + "?action=time"); // RELATIVE — prefix-safe
|
|
160
|
+
el.textContent = JSON.stringify(await r.json(), null, 2);
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
163
|
+
</body></html>`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function json(body: unknown, status = 200): Response {
|
|
167
|
+
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...cors() } });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Deno.serve((req: Request) => {
|
|
171
|
+
const url = new URL(req.url);
|
|
172
|
+
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors() });
|
|
173
|
+
|
|
174
|
+
const action = url.searchParams.get("action");
|
|
175
|
+
if (action === "time") return json({ ok: true, now: new Date().toISOString() });
|
|
176
|
+
if (action) return json({ ok: false, error: `unknown action: ${action}` }, 400);
|
|
177
|
+
|
|
178
|
+
return new Response(pageHtml(new Date().toISOString()),
|
|
179
|
+
{ status: 200, headers: { "Content-Type": "text/html; charset=utf-8", ...cors() } });
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Full example — `api` (headless JSON backend)
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
function cors(): Record<string, string> {
|
|
187
|
+
return {
|
|
188
|
+
"Access-Control-Allow-Origin": "*",
|
|
189
|
+
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
|
|
190
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, X-Tenant-Id",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function json(body: unknown, status = 200): Response {
|
|
194
|
+
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...cors() } });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Deno.serve(async (req: Request) => {
|
|
198
|
+
const url = new URL(req.url);
|
|
199
|
+
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors() });
|
|
200
|
+
|
|
201
|
+
const action = url.searchParams.get("action") ?? "health";
|
|
202
|
+
switch (action) {
|
|
203
|
+
case "health":
|
|
204
|
+
return json({ ok: true, now: new Date().toISOString() });
|
|
205
|
+
case "echo": {
|
|
206
|
+
let body: unknown = null;
|
|
207
|
+
try { body = await req.json(); } catch { return json({ ok: false, error: "invalid JSON body" }, 400); }
|
|
208
|
+
return json({ ok: true, echo: body });
|
|
209
|
+
}
|
|
210
|
+
default:
|
|
211
|
+
return json({ ok: false, error: `unknown action: ${action}` }, 400);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Multi-page mode (real static MPA)
|
|
217
|
+
|
|
218
|
+
A classic MPA — distinct `index.html`, `about.html`, `style.css`, `app.js` reached **by path** — is now supported via the `mpa` template. Start with:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
pxbaas app new site --template mpa
|
|
222
|
+
pxbaas app deploy site
|
|
223
|
+
pxbaas app verify site # probes every page + asset for 200 + content-type
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
How it works (Plan B): `app.json` declares `"mode": "mpa"`. On deploy, `app.json` and every static file are uploaded to `edge_function_files`. The **proxy static branch** reads `mode` back and, for `mpa` functions only, serves the stored files directly by path with correct MIME + `ETag` (function version) + `Cache-Control`. The Deno worker (`index.ts`) is reached only for the JSON API (`?action=...`) and for paths that are not stored static files.
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
GET /functions/v1/site/ → static index.html (text/html)
|
|
230
|
+
GET /functions/v1/site/about.html → static about.html (text/html)
|
|
231
|
+
GET /functions/v1/site/style.css → static style.css (text/css)
|
|
232
|
+
GET /functions/v1/site/app.js → static app.js (text/javascript)
|
|
233
|
+
GET /functions/v1/site/?action=time → worker JSON API (application/json)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Hard constraints (spec-lint enforces):
|
|
237
|
+
|
|
238
|
+
- **Reference assets with RELATIVE links.** `<link href="style.css">`, `<script src="app.js">`, `<a href="about.html">`. Root-absolute (`href="/style.css"`) escapes the `/functions/v1/{slug}` prefix and is blocked (`no-root-absolute`) — scanned across the entrypoint **and** the mpa html/css/js files.
|
|
239
|
+
- **Every declared page/asset must exist on disk.** `app.json` `pages`/`assets` entries that are missing from the app directory are a blocker (`mpa-assets-exist`) — otherwise the proxy falls through and the browser gets a 404.
|
|
240
|
+
- **API calls need the directory base.** From `about.html`, `fetch("?action=time")` would resolve to `about.html?action=time` (a static path). Use a computed base (`location.pathname` up to the trailing `/`) + `?action=...`, or `./?action=...`, so the call hits the worker at the function root.
|
|
241
|
+
- **Keep files small.** Each static file must be ≤ `STATIC_MAX_BYTES` (default 256 KB) or it falls through to the worker.
|
|
242
|
+
- **Text files only.** Static serving covers text (`.html/.css/.js/.mjs/.svg/.json/.map/.txt`). **Binary assets (png/jpg/gif/webp/ico/woff/woff2/ttf/…) are blocked at deploy** (`no-binary-assets`) because the utf-8 upload pipeline corrupts them — host them on **Storage/OSS** and reference by URL.
|
|
243
|
+
- **Nested dirs are allowed.** You may organize assets under `assets/`, `pages/`, etc.; the CLI preserves the relative path. List nested paths (`assets/style.css`) in `app.json` `pages`/`assets` and link them relatively.
|
|
244
|
+
- **HTML revalidates; assets cache.** HTML is served `no-cache` (redeploy shows immediately); other text assets get `max-age` + `ETag`=version.
|
|
245
|
+
- **No secrets in static files.** mpa files are publicly readable by design; never inline keys.
|
|
246
|
+
|
|
247
|
+
### Worker entrypoint (`index.ts`) for mpa
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
function cors(): Record<string, string> {
|
|
251
|
+
return {
|
|
252
|
+
"Access-Control-Allow-Origin": "*",
|
|
253
|
+
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
|
|
254
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, X-Tenant-Id",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function json(body: unknown, status = 200): Response {
|
|
258
|
+
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...cors() } });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
Deno.serve((req: Request) => {
|
|
262
|
+
const url = new URL(req.url);
|
|
263
|
+
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors() });
|
|
264
|
+
|
|
265
|
+
// Static pages/assets are served by the proxy BEFORE reaching here.
|
|
266
|
+
const action = url.searchParams.get("action");
|
|
267
|
+
if (action === "time") return json({ ok: true, now: new Date().toISOString() });
|
|
268
|
+
if (action) return json({ ok: false, error: `unknown action: ${action}` }, 400);
|
|
269
|
+
return json({ ok: false, error: "not found" }, 404);
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Single-file alternatives (no mpa)
|
|
274
|
+
|
|
275
|
+
If you prefer to stay in one file (`spa`/`api`), multi-view UX is still possible without the static branch:
|
|
276
|
+
|
|
277
|
+
| pattern | mechanism | use for |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| **client-side hash routing** | one HTML shell; JS switches views on `location.hash` (`#/home`, `#/about`) | standard multi-view demos; hash is inherently prefix-safe |
|
|
280
|
+
| **server-branched virtual pages** | the single handler returns a *different inline HTML* per `?page=` branch | per-page SSR HTML while staying in one function |
|
|
281
|
+
| **multiple slugs** | deploy each page/site as its own slug | truly independent pages; costs function quota (5) and splits URLs |
|
|
282
|
+
|
|
283
|
+
For these, links must be relative or hash (`href="?page=about"`, `href="#/about"`), never root-absolute.
|
|
284
|
+
|
|
285
|
+
## Reading data in legacy functions
|
|
286
|
+
|
|
287
|
+
Call the instance's own REST API with the **anon** key server-side; never inline keys into client HTML:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const ANON = Deno.env.get("SUPABASE_ANON_KEY") ?? ""; // provided server-side, not in the page
|
|
291
|
+
async function listTodos() {
|
|
292
|
+
const base = new URL(req.url).origin; // same gateway
|
|
293
|
+
const r = await fetch(`${base}/rest/v1/todos?select=*&limit=20`, {
|
|
294
|
+
headers: { apikey: ANON, Authorization: `Bearer ${ANON}` },
|
|
295
|
+
});
|
|
296
|
+
return await r.json();
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
(Direct MySQL drivers such as `npm:mysql2` are **not supported** in the first release — access data through the instance REST/Auth API. The npm allow-list admits only Hono and the capability `ssrLibs` renderers; anything else fails spec-lint. See `/docs` for MySQL-vs-Postgres limitations.)
|
|
300
|
+
|
|
301
|
+
## Editing guidance for agents
|
|
302
|
+
|
|
303
|
+
- Start from one of the bundled templates (`pxbaas app templates` lists them); edit within the scaffolded structure.
|
|
304
|
+
- Manifest v2 React: keep exact dependency versions and lockfile, use ordinary CSS and `HashRouter`, build with `npm ci && npm run build`, and deploy only `dist`.
|
|
305
|
+
- Manifest v2 backend: keep `Deno.serve`, no runtime Secrets/egress, and retain the Hono placeholder when applicable.
|
|
306
|
+
- Legacy single-file (`spa`/`api`): keep every asset inline and every client call relative.
|
|
307
|
+
- Legacy `mpa`: list real text files under `pages`/`assets`, link them relatively, and keep the worker for `?action=` only.
|
|
308
|
+
- Run `pxbaas app capabilities --json` and `pxbaas app deploy --dry-run --json` before publishing.
|