@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.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/.claude/skills/urlcode-authoring/SKILL.md +106 -0
- package/.claude/skills/urlcode-operations/SKILL.md +114 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/CONTRIBUTING.md +30 -2
- package/README.md +157 -230
- package/ROADMAP.md +85 -2
- package/SECURITY.md +8 -1
- package/dist/BUILD-MANIFEST.json +62 -32
- package/dist/adapters.js +4 -23
- package/dist/agent-lists.js +1 -1
- package/dist/agents-guide.js +113 -0
- package/dist/authoring-files.js +60 -0
- package/dist/authoring.js +11 -1
- package/dist/aws.js +4 -3
- package/dist/build-cloudflare.js +11 -24
- package/dist/bulk.js +37 -0
- package/dist/capabilities.js +208 -0
- package/dist/capability-query.js +72 -0
- package/dist/catalog.js +105 -0
- package/dist/cli.js +154 -15
- package/dist/client-address.js +1 -1
- package/dist/compliance-rules/baseline.js +8 -8
- package/dist/compliance-rules/privacy.js +3 -3
- package/dist/compliance-rules/strict.js +5 -5
- package/dist/conditions.js +88 -0
- package/dist/config.js +60 -4
- package/dist/context.js +156 -0
- package/dist/ecosystem-cli.js +88 -0
- package/dist/egress.js +98 -0
- package/dist/examples.js +92 -0
- package/dist/explain-cli.js +64 -0
- package/dist/explain.js +132 -0
- package/dist/extensions.js +172 -0
- package/dist/function-sources.js +15 -3
- package/dist/index.js +37 -0
- package/dist/init-with.js +165 -0
- package/dist/interchange-cli.js +42 -0
- package/dist/interchange.js +189 -0
- package/dist/link-cli.js +1 -1
- package/dist/management-policy.js +0 -1
- package/dist/manifest.js +111 -0
- package/dist/match.js +2 -2
- package/dist/mcp-authoring.js +147 -0
- package/dist/mcp.js +97 -0
- package/dist/observability.js +6 -0
- package/dist/operator-host.js +29 -0
- package/dist/plugins.js +12 -0
- package/dist/policies/agents.js +2 -2
- package/dist/policies/compression.js +2 -1
- package/dist/policies/security.js +0 -0
- package/dist/policies.js +1 -1
- package/dist/policy.js +29 -7
- package/dist/prerender.js +100 -41
- package/dist/project-tests.js +3 -3
- package/dist/provider-verification.js +92 -0
- package/dist/proxy.js +44 -0
- package/dist/readiness.js +5 -5
- package/dist/recipes.js +41 -0
- package/dist/route-diff.js +106 -0
- package/dist/router.js +42 -2
- package/dist/runtime.js +95 -14
- package/dist/schema-query.js +62 -0
- package/dist/signals.js +24 -0
- package/dist/site.js +0 -0
- package/dist/tooling.js +96 -0
- package/dist/types/adapters.d.ts +7 -4
- package/dist/types/agent-lists.d.ts +0 -1
- package/dist/types/agents-guide.d.ts +17 -0
- package/dist/types/authoring-files.d.ts +10 -0
- package/dist/types/aws.d.ts +3 -1
- package/dist/types/build-cloudflare.d.ts +1 -0
- package/dist/types/bulk.d.ts +27 -0
- package/dist/types/capabilities.d.ts +55 -0
- package/dist/types/capability-query.d.ts +24 -0
- package/dist/types/catalog.d.ts +65 -0
- package/dist/types/client-address.d.ts +0 -1
- package/dist/types/compliance-rules/baseline.d.ts +1 -9
- package/dist/types/compliance-rules/privacy.d.ts +1 -4
- package/dist/types/compliance-rules/strict.d.ts +0 -5
- package/dist/types/conditions.d.ts +19 -0
- package/dist/types/config.d.ts +20 -2
- package/dist/types/context.d.ts +65 -0
- package/dist/types/ecosystem-cli.d.ts +17 -0
- package/dist/types/egress.d.ts +46 -0
- package/dist/types/examples.d.ts +50 -0
- package/dist/types/explain-cli.d.ts +11 -0
- package/dist/types/explain.d.ts +95 -0
- package/dist/types/extensions.d.ts +122 -0
- package/dist/types/function-sources.d.ts +5 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/init-with.d.ts +30 -0
- package/dist/types/interchange-cli.d.ts +16 -0
- package/dist/types/interchange.d.ts +42 -0
- package/dist/types/link-cli.d.ts +2 -1
- package/dist/types/management-policy.d.ts +0 -1
- package/dist/types/manifest.d.ts +81 -0
- package/dist/types/match.d.ts +1 -0
- package/dist/types/mcp-authoring.d.ts +92 -0
- package/dist/types/mcp.d.ts +12 -0
- package/dist/types/observability.d.ts +2 -0
- package/dist/types/operator-host.d.ts +8 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/policies/agents.d.ts +0 -2
- package/dist/types/policies/compression.d.ts +2 -0
- package/dist/types/policies/security.d.ts +0 -1
- package/dist/types/policy.d.ts +15 -4
- package/dist/types/project-tests.d.ts +3 -1
- package/dist/types/provider-verification.d.ts +53 -0
- package/dist/types/proxy.d.ts +21 -0
- package/dist/types/readiness.d.ts +1 -1
- package/dist/types/recipes.d.ts +30 -0
- package/dist/types/route-diff.d.ts +27 -0
- package/dist/types/runtime.d.ts +11 -0
- package/dist/types/schema-query.d.ts +12 -0
- package/dist/types/signals.d.ts +25 -0
- package/dist/types/site.d.ts +0 -1
- package/dist/types/tooling.d.ts +115 -0
- package/dist/types/types.d.ts +57 -0
- package/dist/types/typescript-authoring.d.ts +12 -0
- package/dist/types/vercel.d.ts +3 -1
- package/dist/types/verify-deployment.d.ts +47 -0
- package/dist/types.js +21 -2
- package/dist/typescript-authoring.js +104 -0
- package/dist/vercel.js +4 -3
- package/dist/verify-deployment.js +270 -0
- package/docs/AI-AUTHORING.md +130 -8
- package/docs/BULK.md +79 -0
- package/docs/CAPABILITIES.md +179 -0
- package/docs/CAPACITY.md +1 -1
- package/docs/CI.md +142 -0
- package/docs/CONDITIONS.md +74 -0
- package/docs/DEPLOYMENT-CHECKS.md +108 -0
- package/docs/DYNAMIC-LINKS.md +18 -518
- package/docs/EGRESS.md +125 -0
- package/docs/EXTENSIONS.md +226 -0
- package/docs/FRAMEWORK.md +182 -0
- package/docs/INSTALL.md +45 -7
- package/docs/INTERCHANGE.md +134 -0
- package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
- package/docs/MIDDLEWARE.md +2 -0
- package/docs/NEXT-PHASE-PLAN.md +90 -0
- package/docs/NEXT-STEPS.md +415 -0
- package/docs/OBSERVABILITY.md +4 -2
- package/docs/OPERATIONAL-PROOF.md +4 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/PLUGINS.md +37 -0
- package/docs/POLICIES.md +12 -309
- package/docs/PRERENDER.md +40 -0
- package/docs/PROJECT-DIRECTION.md +42 -0
- package/docs/PROVIDER-VERIFICATION.md +84 -0
- package/docs/READINESS.md +21 -1
- package/docs/README.md +82 -31
- package/docs/RECIPES.md +99 -0
- package/docs/RELEASE-READINESS.md +11 -9
- package/docs/RELEASE-SECURITY.md +27 -4
- package/docs/SECURITY-AUDIT.md +1 -1
- package/docs/SPECIFICATION.md +95 -8
- package/docs/SPIKE-BUSINESS-SUITE.md +1013 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
- package/docs/SPIKE-EXTENSIONS.md +1 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +199 -0
- package/docs/STANDARDS.md +150 -142
- package/docs/STARTERS.md +21 -1
- package/docs/TOOLING.md +291 -0
- package/docs/TYPESCRIPT-AUTHORING.md +67 -0
- package/docs/TYPESCRIPT.md +1 -1
- package/docs/USABILITY-REVIEW.md +123 -0
- package/docs/YAML-GUIDE.md +18 -479
- package/docs/YAML-REFERENCE.md +127 -16
- package/docs/links/cli.md +110 -0
- package/docs/links/limits.md +175 -0
- package/docs/links/management-api.md +80 -0
- package/docs/links/pools.md +75 -0
- package/docs/links/setup.md +135 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/contract.md +52 -0
- package/docs/policies/hardened.md +56 -0
- package/docs/policies/interoperability.md +169 -0
- package/docs/policies/operations.md +45 -0
- package/docs/yaml/assets.md +36 -0
- package/docs/yaml/conditions.md +20 -0
- package/docs/yaml/functions.md +160 -0
- package/docs/yaml/links.md +30 -0
- package/docs/yaml/middleware.md +29 -0
- package/docs/yaml/organization.md +74 -0
- package/docs/yaml/policies.md +37 -0
- package/docs/yaml/redirects.md +64 -0
- package/docs/yaml/responses.md +57 -0
- package/docs/yaml/site.md +24 -0
- package/examples/assets/example.yaml +17 -0
- package/examples/aws/example.yaml +20 -0
- package/examples/cloudflare/example.yaml +19 -0
- package/examples/compliance/example.yaml +11 -0
- package/examples/conditions/README.md +12 -0
- package/examples/conditions/example.yaml +19 -0
- package/examples/conditions/tests/requests.json +13 -0
- package/examples/conditions/urlcode.yaml +24 -0
- package/examples/cookbook/README.md +8 -4
- package/examples/cookbook/example.yaml +17 -0
- package/examples/cookbook/functions/catalog.mjs +3 -0
- package/examples/cookbook/functions/fail.mjs +4 -0
- package/examples/cookbook/functions/items.mjs +3 -0
- package/examples/cookbook/functions/profile.mjs +3 -0
- package/examples/cookbook/functions/resource.mjs +3 -0
- package/examples/cookbook/functions/status.mjs +3 -0
- package/examples/cookbook/middleware/auth.mjs +48 -0
- package/examples/cookbook/middleware/body.mjs +15 -0
- package/examples/cookbook/middleware/bucket.mjs +19 -0
- package/examples/cookbook/middleware/cors.mjs +21 -0
- package/examples/cookbook/middleware/debug.mjs +13 -0
- package/examples/cookbook/middleware/envelope.mjs +11 -0
- package/examples/cookbook/middleware/errors.mjs +11 -0
- package/examples/cookbook/middleware/etag.mjs +18 -0
- package/examples/cookbook/middleware/locale.mjs +16 -0
- package/examples/cookbook/middleware/maintenance.mjs +10 -0
- package/examples/cookbook/middleware/methods.mjs +15 -0
- package/examples/cookbook/middleware/negotiate.mjs +20 -0
- package/examples/cookbook/middleware/referer.mjs +12 -0
- package/examples/cookbook/middleware/request-id.mjs +16 -0
- package/examples/cookbook/route-index.json +676 -0
- package/examples/cookbook/routes/middleware.yaml +126 -0
- package/examples/cookbook/tests/requests.json +526 -0
- package/examples/cookbook/urlcode.yaml +1 -0
- package/examples/egress/README.md +22 -0
- package/examples/egress/example.yaml +19 -0
- package/examples/egress/urlcode.yaml +19 -0
- package/examples/extensions/README.md +7 -0
- package/examples/extensions/example.yaml +21 -0
- package/examples/extensions/urlcode.yaml +25 -0
- package/examples/live-links/example.yaml +21 -0
- package/examples/monitoring/example.yaml +8 -0
- package/examples/prerender/example.yaml +16 -0
- package/examples/provider-conformance/README.md +12 -0
- package/examples/provider-conformance/example.yaml +14 -0
- package/examples/provider-conformance/urlcode.yaml +34 -0
- package/examples/tunnel/example.yaml +8 -0
- package/examples/vercel/example.yaml +19 -0
- package/llms-full.txt +2709 -0
- package/llms.txt +48 -19
- package/package.json +29 -7
- package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +106 -0
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +114 -0
- package/recipes/authenticated-json-api/README.md +51 -0
- package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
- package/recipes/authenticated-json-api/recipe.yaml +34 -0
- package/recipes/authenticated-json-api/tests/requests.json +39 -0
- package/recipes/authenticated-json-api/urlcode.yaml +12 -0
- package/recipes/contact-form/README.md +25 -0
- package/recipes/contact-form/functions/contact.mjs +17 -0
- package/recipes/contact-form/recipe.yaml +33 -0
- package/recipes/contact-form/tests/requests.json +47 -0
- package/recipes/contact-form/urlcode.yaml +18 -0
- package/recipes/cors-api/README.md +16 -0
- package/recipes/cors-api/functions/items.mjs +3 -0
- package/recipes/cors-api/middleware/cors.mjs +21 -0
- package/recipes/cors-api/recipe.yaml +26 -0
- package/recipes/cors-api/tests/requests.json +65 -0
- package/recipes/cors-api/urlcode.yaml +12 -0
- package/recipes/health-page/README.md +13 -0
- package/recipes/health-page/recipe.yaml +23 -0
- package/recipes/health-page/tests/requests.json +36 -0
- package/recipes/health-page/urlcode.yaml +19 -0
- package/recipes/json-api/README.md +6 -0
- package/recipes/json-api/functions/echo.mjs +3 -0
- package/recipes/json-api/recipe.yaml +25 -0
- package/recipes/json-api/tests/requests.json +34 -0
- package/recipes/json-api/urlcode.yaml +12 -0
- package/recipes/middleware/README.md +34 -0
- package/recipes/middleware/functions/catalog.mjs +3 -0
- package/recipes/middleware/functions/fail.mjs +4 -0
- package/recipes/middleware/functions/items.mjs +3 -0
- package/recipes/middleware/functions/profile.mjs +3 -0
- package/recipes/middleware/functions/resource.mjs +3 -0
- package/recipes/middleware/functions/status.mjs +3 -0
- package/recipes/middleware/middleware/auth.mjs +48 -0
- package/recipes/middleware/middleware/body.mjs +15 -0
- package/recipes/middleware/middleware/bucket.mjs +19 -0
- package/recipes/middleware/middleware/cors.mjs +21 -0
- package/recipes/middleware/middleware/debug.mjs +13 -0
- package/recipes/middleware/middleware/envelope.mjs +11 -0
- package/recipes/middleware/middleware/errors.mjs +11 -0
- package/recipes/middleware/middleware/etag.mjs +18 -0
- package/recipes/middleware/middleware/locale.mjs +16 -0
- package/recipes/middleware/middleware/maintenance.mjs +10 -0
- package/recipes/middleware/middleware/methods.mjs +15 -0
- package/recipes/middleware/middleware/negotiate.mjs +20 -0
- package/recipes/middleware/middleware/referer.mjs +12 -0
- package/recipes/middleware/middleware/request-id.mjs +16 -0
- package/recipes/middleware/public/guide.txt +1 -0
- package/recipes/middleware/recipe.yaml +50 -0
- package/recipes/middleware/tests/requests.json +528 -0
- package/recipes/middleware/urlcode.yaml +127 -0
- package/recipes/protected-download/README.md +22 -0
- package/recipes/protected-download/files/report.txt +1 -0
- package/recipes/protected-download/recipe.yaml +31 -0
- package/recipes/protected-download/tests/requests.json +32 -0
- package/recipes/protected-download/urlcode.yaml +15 -0
- package/recipes/redirect/README.md +7 -0
- package/recipes/redirect/recipe.yaml +25 -0
- package/recipes/redirect/tests/requests.json +19 -0
- package/recipes/redirect/urlcode.yaml +9 -0
- package/recipes/static-plus-api/README.md +15 -0
- package/recipes/static-plus-api/functions/info.mjs +3 -0
- package/recipes/static-plus-api/public/assets/index.html +3 -0
- package/recipes/static-plus-api/public/assets/site.css +1 -0
- package/recipes/static-plus-api/public/index.html +8 -0
- package/recipes/static-plus-api/recipe.yaml +29 -0
- package/recipes/static-plus-api/tests/requests.json +56 -0
- package/recipes/static-plus-api/urlcode.yaml +23 -0
- package/recipes/typescript/README.md +7 -0
- package/recipes/typescript/functions/hello.ts +5 -0
- package/recipes/typescript/recipe.yaml +23 -0
- package/recipes/typescript/tests/requests.json +18 -0
- package/recipes/typescript/urlcode.yaml +5 -0
- package/recipes/webhook-receiver/README.md +16 -0
- package/recipes/webhook-receiver/functions/receive.mjs +16 -0
- package/recipes/webhook-receiver/recipe.yaml +26 -0
- package/recipes/webhook-receiver/tests/requests.json +59 -0
- package/recipes/webhook-receiver/urlcode.yaml +16 -0
- package/schemas/recipe.schema.json +138 -0
- package/schemas/urlcode.schema.json +656 -80
- package/skills/urlcode/SKILL.md +98 -0
- package/starters/default/.github/workflows/urlcode.yml +23 -0
- package/starters/default/.mcp.json +12 -0
- package/starters/default/AGENTS.md +79 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Review: the extension model, its precedents and its alignment
|
|
2
|
+
|
|
3
|
+
Status: review of the [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/docs/SPIKE-AUTH.md), [admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/docs/SPIKE-ADMIN.md) and
|
|
4
|
+
[UI kit](https://github.com/jimhoyd-com/urlcode-ui/blob/main/docs/SPIKE-UI.md) spikes against the runtime's principles and against
|
|
5
|
+
how established frameworks add the same capabilities. Core portability is
|
|
6
|
+
the fixed point: a project's YAML must work unchanged on another host.
|
|
7
|
+
Section 1 is what the review changed; section 2 is what other projects do
|
|
8
|
+
and what was taken from each; section 3 is the case for the model.
|
|
9
|
+
|
|
10
|
+
## 1. Alignment issues found and how they were resolved
|
|
11
|
+
|
|
12
|
+
1. **A YAML-only project has no server file.** The spikes said `init`
|
|
13
|
+
writes "a plugin line into the server file", but projects run through
|
|
14
|
+
`urlcode dev` and `urlcode serve`; the starter is YAML, functions and a
|
|
15
|
+
Makefile. Operator material already loads from explicit paths
|
|
16
|
+
(`--policy`, `--compliance-rules`, `--auth-file`), never from inside the
|
|
17
|
+
project. Resolved: a host file, `host.js`, exporting `{ store, plugins }`,
|
|
18
|
+
loaded with `--host-file <path>` and written by `init`; the starter's
|
|
19
|
+
`make dev` and `make serve` pass it. It is the one file of code an
|
|
20
|
+
extended project has, it is operator code by the same rule as the grant
|
|
21
|
+
file, and it is never discovered by convention inside the project
|
|
22
|
+
because the project is untrusted content.
|
|
23
|
+
2. **`origin` in `auth.yaml` broke portability.** The passkey relying-party
|
|
24
|
+
id and provider redirect base were in the auth block. An origin is a
|
|
25
|
+
deployment fact; the runtime already takes `--origin` for the sitemap
|
|
26
|
+
and compliance. Resolved: removed from YAML, taken from `--origin`.
|
|
27
|
+
3. **`mount` duplicated the route.** The auth block carried `mount:
|
|
28
|
+
/account` while the route `/account/*: { extension: auth }` already
|
|
29
|
+
said where the extension lives. Resolved: the route is the mount.
|
|
30
|
+
4. **Two ways to protect a route.** `protect` path patterns in `auth.yaml`
|
|
31
|
+
and `policies.auth` on routes would drift apart. Policies already have
|
|
32
|
+
profiles for sharing a requirement across routes. Resolved: `protect`
|
|
33
|
+
removed; `policies.auth` on a route or in a profile is the only way.
|
|
34
|
+
5. **`extensions.ui` had no owner.** The runtime seam refuses a block no
|
|
35
|
+
plugin claims, and the kit was described as a library. Resolved: the kit
|
|
36
|
+
ships one small plugin that owns the block and serves the stylesheet and
|
|
37
|
+
scripts at one route, which also answers how two extensions avoid
|
|
38
|
+
serving the same asset twice.
|
|
39
|
+
6. **Extension routes had no fixtures.** The audit generates fixtures for
|
|
40
|
+
native routes from their declarations; it cannot for an extension route.
|
|
41
|
+
Resolved: the plugin supplies fixtures for its routes through the
|
|
42
|
+
existing `testPlan` seam, so the audit, `urlcode test` and
|
|
43
|
+
`verify-deployment` cover them.
|
|
44
|
+
7. **Translations were "later".** Every string in the kit and the notices
|
|
45
|
+
is catalogued from the first release, with language negotiation, plural
|
|
46
|
+
rules, locale formatting and RTL-safe templates; English ships, any
|
|
47
|
+
language is a file. Adding this later would have meant re-touching every
|
|
48
|
+
template.
|
|
49
|
+
8. **`__Host-` cookies in development.** The prefix requires `Secure`;
|
|
50
|
+
browsers treat `http://localhost` as a secure context so it works there,
|
|
51
|
+
but not on a LAN address. `urlcode dev` uses a `__Host-` cookie on
|
|
52
|
+
localhost and a plain-named one elsewhere, and `hardened` refuses
|
|
53
|
+
anything but `__Host-` in production. Noted in the auth spike.
|
|
54
|
+
|
|
55
|
+
What did not need changing: the four runtime seams stay generic; the
|
|
56
|
+
store contract stays a document store with declared indexes and no joins;
|
|
57
|
+
the plugin remains the only host-code seam; nothing in YAML names a
|
|
58
|
+
package; every target either enforces or refuses at activation; the
|
|
59
|
+
runtime never depends on an extension.
|
|
60
|
+
|
|
61
|
+
## 2. How other frameworks do this, and what was taken
|
|
62
|
+
|
|
63
|
+
The pattern "core stays small, capabilities arrive as installable pieces
|
|
64
|
+
that bring routes, storage, pages and configuration, and the app overrides
|
|
65
|
+
them by file" is thirty years old. The instructive precedents:
|
|
66
|
+
|
|
67
|
+
| Framework | The piece | Adds routes | Adds storage | Pages and override | Configuration | Taken |
|
|
68
|
+
|---|---|---|---|---|---|---|
|
|
69
|
+
| **Django** | `contrib.auth`, `contrib.admin`, `django-allauth` | `include()` in `urls.py` | models plus migrations | template override by path (`templates/account/login.html` shadows the app's); admin registered per model | `settings.py` keys per app | The include model, per-path template shadowing, auth and admin as two apps where admin builds on auth |
|
|
70
|
+
| **Rails** | engines: Devise, ActiveAdmin | `devise_for` route helper, `mount Engine => "/admin"` | migrations generated into the app | `rails generate devise:views` copies views into the app to edit (ejecting); copy in `config/locales/devise.en.yml` | initializer file | Eject as the override mechanism; copy in a locale YAML file with ids, the origin of the copy catalogue |
|
|
71
|
+
| **Laravel** | Fortify (headless auth) plus Breeze or Jetstream (UI), Filament (admin) | service provider registers routes | migrations published | `php artisan vendor:publish` copies views, config and translations into the app; Filament resources per model | published config file | The split between a headless behaviour package and a UI package, which is the auth versus UI kit split; publish equals eject |
|
|
72
|
+
| **WordPress** | plugins and themes | plugins register rewrite rules | plugin tables | template hierarchy and child themes: the app's file wins over the plugin's | options table, filters | The override order "project file, then extension default, then kit default", and the lesson that logic in templates is where plugin ecosystems rot |
|
|
73
|
+
| **Keycloak** | realms, themes | its own server | its own database | theme directories override templates, CSS and messages per realm | realm JSON | Message bundles per theme; the account console's surface |
|
|
74
|
+
| **Ory Kratos** | identity schemas, self-service flows | its own server | its own database | no UI, flow JSON for any renderer; Elements as optional components | identity schema JSON | Flows as resumable records with ids; identifiers, traits and credentials modelled apart |
|
|
75
|
+
| **Better Auth** | server plugins with a client counterpart | plugin declares endpoints | plugin declares schema, CLI generates migrations | no UI | TypeScript config | An extension declares its routes and collections and the tooling derives the rest; hooks for the app |
|
|
76
|
+
| **Supabase Auth**, Auth.js, Lucia | libraries or a service | fixed endpoints | fixed schema | none | config object | Mostly what to avoid: UI is always the app's job, so every app rebuilds the same pages |
|
|
77
|
+
| **Payload CMS**, Strapi | plugins that inject into the admin | yes | collections | admin injection points | config | Admin as an extension that other extensions can add sections to, reserved for later |
|
|
78
|
+
|
|
79
|
+
Two observations from the table shape the model:
|
|
80
|
+
|
|
81
|
+
- **Every mature framework ended up with the same four things**: a way to
|
|
82
|
+
include routes, a way to add storage with migrations, a way to override
|
|
83
|
+
pages by file, and a configuration surface per piece. The runtime's four
|
|
84
|
+
seams are those four things, named for this runtime.
|
|
85
|
+
- **The frameworks that ship a UI with the capability (Django admin,
|
|
86
|
+
Devise views, Breeze, Filament, Keycloak) are the ones people reach for
|
|
87
|
+
first**, and the ones that leave UI to the app (Kratos, Supabase, Auth.js)
|
|
88
|
+
are the ones where every project rebuilds the same sign-in page. The kit
|
|
89
|
+
and the accounts page are the difference.
|
|
90
|
+
|
|
91
|
+
Where this model departs from all of them, deliberately: the
|
|
92
|
+
configuration is YAML validated by a schema rather than code, so a project
|
|
93
|
+
is data that can be checked, diffed, audited and moved; the extension
|
|
94
|
+
brings no framework of its own to the browser; and the trust boundary
|
|
95
|
+
between the project (untrusted) and the operator (host code) is explicit,
|
|
96
|
+
which none of the precedents have because they assume the app author and
|
|
97
|
+
the operator are the same person.
|
|
98
|
+
|
|
99
|
+
## 3. Does this let a builder, human or AI, build less?
|
|
100
|
+
|
|
101
|
+
Yes, with conditions. The claim is not that a framework makes building
|
|
102
|
+
faster in general; it is that this shape removes the specific work that is
|
|
103
|
+
both repetitive and dangerous to get wrong.
|
|
104
|
+
|
|
105
|
+
What it removes:
|
|
106
|
+
|
|
107
|
+
- **Auth, accounts and admin are the parts every product needs and the
|
|
108
|
+
parts most often built badly.** Password storage, session fixation,
|
|
109
|
+
enumeration, CSRF, open redirects, recovery abuse: an AI writing these
|
|
110
|
+
from scratch reproduces the average of what it has read, and the average
|
|
111
|
+
is not good. A declared method with the decisions already made is safer
|
|
112
|
+
than a well-prompted rewrite.
|
|
113
|
+
- **Declaration is a smaller target than code.** A schema-validated YAML
|
|
114
|
+
key has a few valid values; a hand-written sign-in flow has unbounded
|
|
115
|
+
ways to be subtly wrong. Generation against a schema, with `validate`,
|
|
116
|
+
`audit` and `test` as the loop, is where an AI is reliable. The runtime
|
|
117
|
+
already leans this way (`llms.txt`, the YAML reference, executable
|
|
118
|
+
examples with fixtures).
|
|
119
|
+
- **Overrides by file, not by fork.** Restyling by theme variables and
|
|
120
|
+
ejecting one template is work an AI does well and cannot break the flow
|
|
121
|
+
with, because the template cannot change behaviour.
|
|
122
|
+
- **Portability means the work survives the next decision.** A project
|
|
123
|
+
that moves from a laptop to a server to a Worker keeps its YAML; the
|
|
124
|
+
builder does not redo the product to change hosts.
|
|
125
|
+
|
|
126
|
+
The conditions, without which the claim fails:
|
|
127
|
+
|
|
128
|
+
- **Coverage.** The extensions must cover what a typical product needs;
|
|
129
|
+
if the first thing a builder wants is missing, they are back to building.
|
|
130
|
+
The scope cut in the auth spike is a bet that passwords, passkeys, email
|
|
131
|
+
codes, roles, an accounts page and an admin cover the first ten
|
|
132
|
+
products. That bet should be checked against the first three real ones.
|
|
133
|
+
- **The escape hatch must be as easy as the declaration.** The twenty
|
|
134
|
+
percent that is product-specific goes into functions in the WASM guest
|
|
135
|
+
with granted bindings. If that path is harder than declaring, builders
|
|
136
|
+
will route around the runtime.
|
|
137
|
+
- **Documentation written for a reader with no history.** An AI has read
|
|
138
|
+
a great deal of Django and Rails and nothing of this runtime. The
|
|
139
|
+
schema, the reference, the cookbook and `llms.txt` are the training
|
|
140
|
+
data; every extension needs the same set on day one, and `init` must
|
|
141
|
+
print what it did in words a first-time reader can follow.
|
|
142
|
+
- **Small surface, stable contracts.** The advantage disappears if the
|
|
143
|
+
YAML keys churn. View-model versioning, the seam contracts and the store
|
|
144
|
+
contract are the promises; they should change rarely and loudly.
|
|
145
|
+
|
|
146
|
+
The honest comparison is not "this framework versus writing it by hand".
|
|
147
|
+
It is "this framework versus Clerk, Supabase or Firebase", which also let
|
|
148
|
+
a builder skip auth. Those win on time to first sign-in today and lose on
|
|
149
|
+
portability, on cost at scale, on data ownership and on running offline or
|
|
150
|
+
on a Worker. This model's claim to be better for a builder is that it
|
|
151
|
+
offers the same skip with the YAML, the data and the pages staying theirs.
|
|
152
|
+
That is a real position, and it holds only while the runtime stays as
|
|
153
|
+
portable as it is now.
|
|
154
|
+
|
|
155
|
+
## 4. Against Clerk
|
|
156
|
+
|
|
157
|
+
Clerk is the product a developer reaches for when they want auth, an
|
|
158
|
+
accounts page and organizations without building any of it, so it is the
|
|
159
|
+
right yardstick. Feature by feature, with Clerk as of 2026:
|
|
160
|
+
|
|
161
|
+
| Clerk has | This model, first release | Gap |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| Prebuilt sign-in, sign-up, user profile, user button components (React, Next.js, Expo, iOS, Android) | Server-rendered pages on the kit, restyled by theme and ejected templates; no component library | Drop-in React components. Cut for now; a `urlcode-ui/react` package is the answer if React apps are the audience |
|
|
164
|
+
| Hosted account portal | Self-hosted accounts page at `/account` | None: self-hosted is the point |
|
|
165
|
+
| Passwords, passkeys, email code, magic link, SMS code, 20+ social providers, Web3 wallets | Passwords, passkeys, email code, Google, Apple, plus any OpenID Connect provider by issuer URL (added below) | Named buttons and icons for the long tail of providers; SMS; Web3 |
|
|
166
|
+
| MFA: TOTP, SMS, backup codes | TOTP, passkey, recovery codes | SMS as a factor (deliberately) |
|
|
167
|
+
| Multi-session: several accounts signed in, switch between them | One session per browser | Account switching. Small to add later; the session model allows it |
|
|
168
|
+
| Organizations: roles, permissions, invitations, domain auto-join, switcher, B2B SSO (SAML, OIDC), SCIM | Column reserved; nothing else | The largest gap, and Clerk's moat for B2B. Planned, not first release |
|
|
169
|
+
| Bot protection (Turnstile built in), disposable-email blocking, email and domain allowlist and blocklist, sign-up restrictions, waitlist mode | `challenge` hook, honeypot, velocity limits; allowlist, blocklist, disposable list and waitlist added below | A shipped Turnstile adapter, added below |
|
|
170
|
+
| User metadata: public, private, unsafe per user | Added below as `metadata` on the account with the same three visibilities | |
|
|
171
|
+
| Impersonation, dashboard with analytics, user management UI | Admin extension: dashboard and full user management | Same shape, ships one release later |
|
|
172
|
+
| Webhooks (Svix) for every event | Observability events and host hooks | A webhook sender with signing and retries; added to the later list |
|
|
173
|
+
| JWT templates and integrations (Supabase, Hasura, Convex) | Sessions are opaque; no token issuance | Issuing tokens for third-party services comes with "being a provider", later |
|
|
174
|
+
| Email and SMS template editor in the dashboard | Templates as files in the copy catalogue, previewable with `preview` | An editor. Files are the deliberate choice: reviewable, portable |
|
|
175
|
+
| Localization: many languages shipped | Mechanism day one; English shipped | Translated catalogues. Community and native review needed |
|
|
176
|
+
| Theming: appearance prop, CSS variables, themes | Theme variables, ejected templates, own stylesheet | None |
|
|
177
|
+
| Testing tokens, test mode | Test mode with deterministic codes and a fake identity provider | None |
|
|
178
|
+
| SOC 2 Type II, HIPAA BAA, GDPR DPA as the vendor's paper | Compliance evidence export, audit log, retention, a security review before 1.0 | The paper itself. A self-hosted product cannot hand over a vendor's certification; it hands over the evidence for the operator's own |
|
|
179
|
+
| Managed infrastructure, uptime, free tier to 10,000 monthly users, then per-user pricing | Runs on the operator's host; no per-user cost; no one to page | The absence of a vendor is both the gap and the reason |
|
|
180
|
+
|
|
181
|
+
Added to the first release from this comparison, because each is small
|
|
182
|
+
and each is something a builder would notice missing on day one:
|
|
183
|
+
|
|
184
|
+
- **Any OpenID Connect provider** by issuer URL and client id, with
|
|
185
|
+
discovery, beside the named Google and Apple: `oidc: { okta: { issuer:
|
|
186
|
+
… } }`. Named providers are sugar over this.
|
|
187
|
+
- **Account `metadata`** with `public`, `private` and `unsafe` scopes:
|
|
188
|
+
public is readable by the guest binding and the accounts page, private
|
|
189
|
+
only by the host and admin, unsafe writable by the user. Declared
|
|
190
|
+
fields with types in YAML, so it is still a schema.
|
|
191
|
+
- **Sign-up controls**: `registration: open | invite-only | waitlist |
|
|
192
|
+
off`, with an allowlist and blocklist of emails and domains and the
|
|
193
|
+
bundled disposable-domain list.
|
|
194
|
+
- **A Turnstile adapter** for the `challenge` hook (and the hook stays
|
|
195
|
+
vendor-neutral; hCaptcha and reCAPTCHA adapters are a few lines each).
|
|
196
|
+
|
|
197
|
+
Moved onto the later list: multi-session account switching, a signed
|
|
198
|
+
webhook sender with retries, token issuance for third-party services, and
|
|
199
|
+
the React component package.
|
|
200
|
+
|
|
201
|
+
### Will it be the obvious choice?
|
|
202
|
+
|
|
203
|
+
Not for everyone, and it should not try to be. It becomes the obvious
|
|
204
|
+
choice for a specific developer, and that developer is common:
|
|
205
|
+
|
|
206
|
+
- Someone who wants to own the data and the pages, run on their own host
|
|
207
|
+
or a Worker, and never pay per user. Clerk's pricing and hosted portal
|
|
208
|
+
are the reasons people leave it at scale.
|
|
209
|
+
- Someone building with an AI, or as a small team, who wants the whole
|
|
210
|
+
product declared and checked rather than assembled from SDK calls. The
|
|
211
|
+
YAML, the audit and the fixtures are the pitch; Clerk has no equivalent
|
|
212
|
+
of "diff this pull request's route and policy changes".
|
|
213
|
+
- Someone in a regulated or data-residency context who needs the
|
|
214
|
+
evidence, not a vendor's certificate.
|
|
215
|
+
- Someone whose site started as redirects and pages and is adding
|
|
216
|
+
accounts, which is exactly the runtime's on-ramp.
|
|
217
|
+
|
|
218
|
+
It is not the obvious choice, today, for a React or Next.js team that
|
|
219
|
+
wants drop-in components and organizations with SAML this quarter, or for
|
|
220
|
+
a team that wants a vendor to hold the compliance paper. Both are
|
|
221
|
+
reachable: organizations and SSO are the planned second phase, and the
|
|
222
|
+
React package is a cut, not a rejection.
|
|
223
|
+
|
|
224
|
+
What decides it in practice is not the feature table. It is whether a
|
|
225
|
+
developer gets from `npm install` to a working, good-looking sign-in with
|
|
226
|
+
passkeys in under five minutes, whether the docs answer the next question
|
|
227
|
+
before it is asked, and whether the first three real products fit the
|
|
228
|
+
scope. Those three are the work.
|
|
229
|
+
|
|
230
|
+
## 5. Stepping back: is this still one system, and do the extensions make sense?
|
|
231
|
+
|
|
232
|
+
With auth, admin, the kit and the candidates after them, the runtime stops
|
|
233
|
+
being "a portable URL runtime" and becomes a declarative web application
|
|
234
|
+
platform: a small kernel and a set of installable capabilities. That is a
|
|
235
|
+
change of identity and it should be said out loud rather than drift. The
|
|
236
|
+
question is whether the extension shape is the right way to become that,
|
|
237
|
+
against the two alternatives.
|
|
238
|
+
|
|
239
|
+
- **Everything in core**, the Django `contrib` way. Fastest to build and
|
|
240
|
+
the most coherent to document, but every site would carry auth code it
|
|
241
|
+
does not use, the Cloudflare closure would grow, and the runtime's
|
|
242
|
+
promise that a redirect-only project is tiny and portable would erode.
|
|
243
|
+
Rejected.
|
|
244
|
+
- **Separate services**, the Keycloak way: auth as its own server the
|
|
245
|
+
site talks to. Cleanest isolation, but a second process to run, a
|
|
246
|
+
second store, and the accounts page lives somewhere else. It is what
|
|
247
|
+
people leave Keycloak to avoid. Rejected.
|
|
248
|
+
- **Extensions on generic seams**, the Rails engine and Laravel package
|
|
249
|
+
way. Core stays a kernel; a capability is a package that brings routes,
|
|
250
|
+
collections, pages and a YAML block; the operator installs it. This is
|
|
251
|
+
what every long-lived framework converged on, and it is the shape the
|
|
252
|
+
spikes take.
|
|
253
|
+
|
|
254
|
+
So yes, the extensions make sense, on four conditions that the review
|
|
255
|
+
adds to the plan:
|
|
256
|
+
|
|
257
|
+
1. **Name the whole.** The runtime is the kernel; the kit and the
|
|
258
|
+
extensions are the distribution. The README should say "a portable
|
|
259
|
+
runtime for sites and the accounts, admin and forms they grow into",
|
|
260
|
+
and the roadmap should show the path in section 1 of the kit spike.
|
|
261
|
+
The principles do not change; the pitch does.
|
|
262
|
+
2. **Keep customisation in the untrusted tier.** Extensions are trusted
|
|
263
|
+
host code; a project's own logic is untrusted WASM. A builder who
|
|
264
|
+
wants a custom rule in a flow ("only `@acme.com` may register",
|
|
265
|
+
"after sign-up, create a workspace") must not have to write host
|
|
266
|
+
code. Extension lifecycle hooks should be able to call a project
|
|
267
|
+
function in the guest, through a granted binding, with a typed input
|
|
268
|
+
and output. Customisation then stays portable YAML plus a guest
|
|
269
|
+
function, and the host file stays what `init` wrote.
|
|
270
|
+
3. **The store needs aggregates.** A document store with equality
|
|
271
|
+
lookups serves auth, but the admin dashboard and every product feature
|
|
272
|
+
want counts and time buckets. Add `count(where)` and a bucketed count
|
|
273
|
+
by a declared timestamp index to the contract now, so no extension is
|
|
274
|
+
tempted to open the backend directly.
|
|
275
|
+
4. **The Node-free rule needs tooling.** Requiring extension cores to be
|
|
276
|
+
free of Node imports is the price of every target working. It is
|
|
277
|
+
only bearable if the kit ships the closure check and a scaffold
|
|
278
|
+
(`create-urlcode-extension`) that starts an extension in the right
|
|
279
|
+
shape, so third parties can add extensions on the same seams without
|
|
280
|
+
reading the runtime's source.
|
|
281
|
+
|
|
282
|
+
One tension remains and should stay visible: two tiers of trust. An
|
|
283
|
+
operator who installs an extension trusts it completely; a project author
|
|
284
|
+
is trusted with nothing. That is the browser's model (extensions versus
|
|
285
|
+
pages) and it is right for a runtime that hosts other people's YAML, but
|
|
286
|
+
it means the extension repositories carry the security burden of the
|
|
287
|
+
whole system. The review before 1.0, the threat models and the dependency
|
|
288
|
+
policy in the auth spike are that burden made explicit.
|
|
289
|
+
|
|
290
|
+
## 6. How to make it better than the alternatives
|
|
291
|
+
|
|
292
|
+
The feature table in section 4 is the floor. What makes it the choice is
|
|
293
|
+
below, ordered by leverage.
|
|
294
|
+
|
|
295
|
+
1. **Five minutes to a passkey sign-in, visibly.** `npm create urlcode`
|
|
296
|
+
asks three questions and produces a site with auth on; a public demo
|
|
297
|
+
runs the cookbook with the accounts page; the README's first screen is
|
|
298
|
+
that demo. Measure the time and print it in the docs.
|
|
299
|
+
2. **Import from where people are.** Importers for Clerk, Supabase,
|
|
300
|
+
Auth.js and Firebase user exports, including verifying their password
|
|
301
|
+
hashes (bcrypt and PBKDF2 alongside scrypt and Argon2id, recorded per
|
|
302
|
+
hash and upgraded on sign-in). Nobody switches auth if their users
|
|
303
|
+
must reset passwords.
|
|
304
|
+
3. **Built for the AI that builds with it.** A `llms.txt` per package,
|
|
305
|
+
the schema published to SchemaStore for editor completion, errors that
|
|
306
|
+
name the YAML key and the fix, and an MCP server that exposes
|
|
307
|
+
`validate`, `audit`, `test`, `routes --compare` and `doctor` so an
|
|
308
|
+
agent can check its own work before a human sees it. The runtime's
|
|
309
|
+
fixtures and audit already make a project checkable; this makes it
|
|
310
|
+
checkable from inside the tools people build with.
|
|
311
|
+
4. **Extension authoring for third parties.** The scaffold, the closure
|
|
312
|
+
check, the seam contracts as published types, and one worked example
|
|
313
|
+
(`forms`) small enough to read in an hour. A platform with two
|
|
314
|
+
first-party extensions is a product; one with twenty third-party ones
|
|
315
|
+
is an ecosystem.
|
|
316
|
+
5. **Starters that are products.** A links site, a docs site, a
|
|
317
|
+
members-only site and a small SaaS skeleton, each a YAML project with
|
|
318
|
+
fixtures, each the answer to "what does this look like finished".
|
|
319
|
+
6. **Trust made public.** The threat models, the independent review's
|
|
320
|
+
report, the release provenance and the benchmarks published, not
|
|
321
|
+
summarised. This is the answer to "why not a vendor".
|
|
322
|
+
7. **The edge story finished.** Cloudflare with the D1 backend and the
|
|
323
|
+
`--extension` build is the deployment nobody else offers for a full
|
|
324
|
+
accounts system in a Worker; it should be the second target, not the
|
|
325
|
+
fourth.
|
|
326
|
+
8. **Operations that a small team can run.** One store, one export, one
|
|
327
|
+
restore drill, `doctor` for every target, the breach-response
|
|
328
|
+
commands, and the compliance evidence export: the argument that
|
|
329
|
+
self-hosting is not a burden is that these exist.
|
|
330
|
+
|
|
331
|
+
## 7. Forkable by design
|
|
332
|
+
|
|
333
|
+
Apache-2.0 makes forking legal. The design has to make it practical: a
|
|
334
|
+
team should be able to take `urlcode-auth`, change what they disagree
|
|
335
|
+
with, publish `acme-auth`, and have every project that says `extension:
|
|
336
|
+
auth` work with it unchanged. That is only possible if the *name* in the
|
|
337
|
+
YAML is a contract and the package is one implementation of it.
|
|
338
|
+
|
|
339
|
+
**The contract is separate from the implementation.** For each extension
|
|
340
|
+
kind there is a small contract package, owned by the runtime's
|
|
341
|
+
organisation, versioned by semver, with no code that does anything:
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
@jimhoyd/urlcode-auth-contract
|
|
345
|
+
schema/ the JSON schema for extensions.auth and for policies.auth
|
|
346
|
+
routes.md the routes an implementation must serve under its mount and what each returns
|
|
347
|
+
collections/ the store collections, keys and indexes an implementation uses
|
|
348
|
+
view-models/ the typed view model of every page, versioned
|
|
349
|
+
copy/ the catalogue ids and the English strings
|
|
350
|
+
fixtures/ request fixtures every implementation must pass
|
|
351
|
+
conformance/ a test suite that runs against any implementation
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The original and every fork depend on the contract, never on each other.
|
|
355
|
+
A fork that keeps the contract is a drop-in: same YAML, same templates,
|
|
356
|
+
same translations, same admin extension on top. A fork that changes the
|
|
357
|
+
contract picks a new name (`extension: acme-auth`) and its own contract
|
|
358
|
+
package, and is honest about not being a drop-in. The runtime's
|
|
359
|
+
`extensions` seam only cares that exactly one plugin claims a name.
|
|
360
|
+
|
|
361
|
+
**What the runtime provides so forks need nothing private:**
|
|
362
|
+
|
|
363
|
+
- The seam types (`Plugin`, `PolicyModule`, the store contract, the
|
|
364
|
+
context bag, the fixtures shape) as published declarations from
|
|
365
|
+
`@jimhoyd/urlcode`, with semver and a deprecation window. Nothing an
|
|
366
|
+
extension needs is reachable only through an unexported path.
|
|
367
|
+
- `provides: 'auth'` in a plugin's registration, distinct from the
|
|
368
|
+
package name, so `admin` requires "a plugin providing `auth` at
|
|
369
|
+
contract `^1`", not `@jimhoyd/urlcode-auth`.
|
|
370
|
+
- The conformance runner: `urlcode extension conform --contract
|
|
371
|
+
@jimhoyd/urlcode-auth-contract --host-file host.js` runs the fixtures
|
|
372
|
+
and the conformance suite against whatever is installed.
|
|
373
|
+
|
|
374
|
+
**What each extension repository does so a fork is an afternoon, not a
|
|
375
|
+
month:**
|
|
376
|
+
|
|
377
|
+
- One package per repository, the runtime's own CI workflows, release
|
|
378
|
+
scripts and container build copied rather than referenced, so the fork
|
|
379
|
+
builds and releases on its own the day it is created.
|
|
380
|
+
- No product name, colour or URL in code or templates; everything comes
|
|
381
|
+
from the theme block and the catalogue, so a fork is not full of the
|
|
382
|
+
original's branding.
|
|
383
|
+
- No telemetry, no update check, no call home. A fork has nothing to
|
|
384
|
+
remove.
|
|
385
|
+
- `FORKING.md` at the root: what to rename, which contract version the
|
|
386
|
+
code implements, how to run conformance, how to publish under a scope,
|
|
387
|
+
and the trademark rule: the runtime's name is not granted by the
|
|
388
|
+
licence, so a fork is `acme-auth`, not `urlcode-auth-acme`, while
|
|
389
|
+
`provides: 'auth'` stays.
|
|
390
|
+
- The scaffold, `create-urlcode-extension`, creates a new extension in
|
|
391
|
+
the same shape, and `--from @jimhoyd/urlcode-auth` creates a fork with
|
|
392
|
+
the renames done.
|
|
393
|
+
- Contract changes are proposals on the contract repository, not commits
|
|
394
|
+
to an implementation, and the original implementation has no special
|
|
395
|
+
standing there beyond being first.
|
|
396
|
+
|
|
397
|
+
**Why this is worth the extra package.** It is the same discipline the
|
|
398
|
+
runtime applies to YAML: behaviour is declared in a portable document and
|
|
399
|
+
any conforming host runs it. Applied to extensions, the portable document
|
|
400
|
+
is the contract and any conforming implementation serves it. It also
|
|
401
|
+
keeps the original honest: if the contract is good enough to fork
|
|
402
|
+
against, it is good enough to build against.
|
|
403
|
+
|
|
404
|
+
## 8. The work, by repository
|
|
405
|
+
|
|
406
|
+
What sections 5 through 7 add, placed where it belongs. Nothing here is
|
|
407
|
+
started.
|
|
408
|
+
|
|
409
|
+
| Repository | Adds |
|
|
410
|
+
|---|---|
|
|
411
|
+
| `urlcode` (runtime) | The four seams and the store additions (with aggregates); `--host-file`; the Cloudflare `--extension` build option; `provides` and contract-version matching in plugin registration; the conformance runner; published seam types with a deprecation policy; hooks that call a project function in the guest with typed input and verdict; the schema on SchemaStore; `llms.txt` per published entry; an MCP server exposing `validate`, `audit`, `test`, `routes --compare`, `verify-deployment` and `doctor`; `npm create urlcode` with the three questions; the four product starters; the public demo |
|
|
412
|
+
| `urlcode-ui` | The kit; the closure check as a reusable test; `create-urlcode-extension` with `--from`; the worked `forms` example |
|
|
413
|
+
| `urlcode-auth-contract`, `urlcode-admin-contract`, `urlcode-ui-contract` | Schema, routes, collections, view models, copy ids, fixtures, conformance |
|
|
414
|
+
| `urlcode-auth` | The first release as scoped, plus bcrypt and PBKDF2 verification for imported hashes and a generic JSON import; Clerk, Supabase, Auth.js and Firebase importers next; `FORKING.md`; threat model; the pre-1.0 review |
|
|
415
|
+
| `urlcode-admin` | Dashboard and users as specified; requires a provider of `auth`, not a package; `FORKING.md` |
|
|
416
|
+
|
|
417
|
+
Cloudflare moves to the second target after `node`, before Vercel and
|
|
418
|
+
AWS, because it is the deployment no alternative offers for a full
|
|
419
|
+
accounts system.
|
package/docs/SPIKE-EXTENSIONS.md
CHANGED
|
@@ -106,6 +106,7 @@ Ranked by how often a self-hosted operator hits it before the first deploy.
|
|
|
106
106
|
| G9 | Adapters refuse functions/middleware/links | Documented and deliberate, but it means any feature built as guest middleware is also refused on serverless | Design constraint |
|
|
107
107
|
| G10 | No JSON Schema body validation | `request.body.format: json` checks syntax only | Low |
|
|
108
108
|
| G11 | No CORS preflight helper | Documented gap | Low |
|
|
109
|
+
| G12 | No SPA fallback for client-routed apps | [Assets](ASSETS.md) rules it out beside directory listing and trailing-slash redirects; a host plugin cannot supply it either, because an unmatched path throws 404 before the request object or any plugin hook exists | Low |
|
|
109
110
|
|
|
110
111
|
## 4. Design constraints these must respect
|
|
111
112
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Spike: compiling `function` routes into their own Lambdas
|
|
2
|
+
|
|
3
|
+
Status: proposal, nothing implemented. No code in this repository does any of
|
|
4
|
+
this, and nothing here is committed scope.
|
|
5
|
+
|
|
6
|
+
AWS already deploys today. `createLambdaHandler` (`src/aws.ts`) runs a project
|
|
7
|
+
as **one** Lambda behind a Function URL or an API Gateway HTTP API, reading the
|
|
8
|
+
same `urlcode.yaml` that runs locally — see [AWS](AWS.md). What it cannot serve
|
|
9
|
+
is `function`, `middleware` and `link`, which
|
|
10
|
+
`activateNativeOnly` (`src/adapters.ts`) refuses for the whole deployment at
|
|
11
|
+
activation rather than letting individual routes fail per request.
|
|
12
|
+
|
|
13
|
+
This spike asks one question: **is the refusal of `function` a fact about
|
|
14
|
+
Lambda, or a fact about the adapter?** It argues the second, sketches the
|
|
15
|
+
lowering that follows, and is deliberate about what that lowering costs.
|
|
16
|
+
|
|
17
|
+
## 1. Where the refusal actually comes from
|
|
18
|
+
|
|
19
|
+
`src/capabilities.ts` gives the reason:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
capability === 'function' ? 'isolated functions need worker threads and the WASM engine'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That is true of the runtime's *own* mechanism. Isolation for guest code is
|
|
26
|
+
QuickJS inside WebAssembly, driven from worker threads, with the boundaries
|
|
27
|
+
[function security](FUNCTION-SECURITY.md) lists: no `process`, no filesystem,
|
|
28
|
+
no sockets, no `fetch`, a fresh guest heap per invocation, and bindings denied
|
|
29
|
+
unless an operator granted them by exact name.
|
|
30
|
+
|
|
31
|
+
A single Lambda cannot host that engine cheaply, because every cold start pays
|
|
32
|
+
worker startup and WASM instantiation before the first request. So the adapter
|
|
33
|
+
refuses — correctly, for the shape it is.
|
|
34
|
+
|
|
35
|
+
But nothing in that sentence is about Lambda. It is about *one process serving
|
|
36
|
+
every route*. Change the deployment unit and the sentence stops applying.
|
|
37
|
+
|
|
38
|
+
## 2. The lowering
|
|
39
|
+
|
|
40
|
+
Cloudflare already establishes the pattern: where a platform forbids what the
|
|
41
|
+
adapter needs, URLCode **compiles ahead of time** instead of adapting at
|
|
42
|
+
runtime. `urlcode build --target cloudflare` (`src/build-cloudflare.ts`) emits
|
|
43
|
+
an artifact the Worker reads, and refuses at build time anything it cannot
|
|
44
|
+
serve, with the route named — see [Cloudflare](CLOUDFLARE.md).
|
|
45
|
+
|
|
46
|
+
The same move for AWS: a build step emits **one Lambda per `function` route**,
|
|
47
|
+
plus the existing native-handler Lambda for everything else.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
urlcode build --target aws --project . --out dist
|
|
51
|
+
|
|
52
|
+
dist/
|
|
53
|
+
routes/ the native handler Lambda (redirect, respond, page, static, download)
|
|
54
|
+
fn/<route-id>/ one directory per function route
|
|
55
|
+
template.yaml the generated stack
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The guest source becomes the Lambda's handler. There is no QuickJS in the
|
|
59
|
+
request path, because the request never crosses a guest boundary inside a
|
|
60
|
+
process — the process *is* the boundary.
|
|
61
|
+
|
|
62
|
+
This is not a smaller change than it looks. Three things follow from it.
|
|
63
|
+
|
|
64
|
+
## 3. What changes, stated plainly
|
|
65
|
+
|
|
66
|
+
### 3.1 The isolation guarantee is replaced, not preserved
|
|
67
|
+
|
|
68
|
+
This is the claim most likely to be made too early, so it goes first.
|
|
69
|
+
|
|
70
|
+
QuickJS-WASM and a Lambda are both real isolation. They are **not the same
|
|
71
|
+
isolation**, and neither strictly contains the other:
|
|
72
|
+
|
|
73
|
+
| | QuickJS-WASM | per-route Lambda |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| Network | unavailable unless a binding is granted | available by default; must be removed |
|
|
76
|
+
| Filesystem | unavailable | a writable `/tmp`, and the deployment package |
|
|
77
|
+
| Environment | not exposed to the guest | ambient unless scrubbed |
|
|
78
|
+
| Blast radius of an escape | the guest heap | the function's IAM role |
|
|
79
|
+
| Per-invocation state | fresh heap, guaranteed | a warm container may be reused |
|
|
80
|
+
|
|
81
|
+
The two rows that matter most are the last two. A guest that escapes QuickJS
|
|
82
|
+
reaches a heap. A guest that misbehaves in a Lambda reaches **whatever that
|
|
83
|
+
Lambda's execution role can reach** — so the compiler would have to emit a role
|
|
84
|
+
per route that grants exactly the route's declared bindings and nothing else,
|
|
85
|
+
and that emitted role becomes a security-critical generated artifact.
|
|
86
|
+
Warm-container reuse is the other: the runtime currently *guarantees* fresh
|
|
87
|
+
state per invocation, and Lambda does not.
|
|
88
|
+
|
|
89
|
+
The honest framing, and the one the docs would have to carry: per-route Lambdas
|
|
90
|
+
are **a substitute for the sandbox, not the sandbox**. Anything that says
|
|
91
|
+
"functions now work on AWS" without saying which guarantee changed is a claim
|
|
92
|
+
this project should not make.
|
|
93
|
+
|
|
94
|
+
### 3.2 `middleware` is the hard part, not `function`
|
|
95
|
+
|
|
96
|
+
`function` lowers cleanly because it is a leaf. `middleware` is a per-request
|
|
97
|
+
chain, and there are only two ways to lower it, both with a real cost:
|
|
98
|
+
|
|
99
|
+
- **Inline** the chain into each function Lambda at build time. Cheap at
|
|
100
|
+
runtime; duplicates the middleware into every function's package, and a
|
|
101
|
+
middleware change rebuilds every function.
|
|
102
|
+
- **Orchestrate** — a hop per middleware. Composable; adds a Lambda invocation
|
|
103
|
+
of latency and cost to every request, on the hot path.
|
|
104
|
+
|
|
105
|
+
Neither is obviously right, which is exactly why this spike scopes middleware
|
|
106
|
+
out rather than picking one under time pressure.
|
|
107
|
+
|
|
108
|
+
### 3.3 `link` does not fall out of this at all
|
|
109
|
+
|
|
110
|
+
Stored live links need a durable writable store that instances share. That is
|
|
111
|
+
the same refusal before and after this change. DynamoDB is the natural lowering,
|
|
112
|
+
but it is a store implementation with its own export and restore discipline
|
|
113
|
+
([dynamic links](DYNAMIC-LINKS.md)), not something a compile step produces.
|
|
114
|
+
|
|
115
|
+
## 4. Emitting infrastructure is a new kind of output
|
|
116
|
+
|
|
117
|
+
`examples/aws/template.yaml` is hand-written today. Generating a stack means
|
|
118
|
+
this project starts owning a surface it has never owned:
|
|
119
|
+
|
|
120
|
+
- The generated template is only as correct as the provider's current
|
|
121
|
+
behaviour, which changes without asking.
|
|
122
|
+
- A generated IAM role is a security artifact (3.1), so "the template is
|
|
123
|
+
internal, don't rely on it" is a weaker disclaimer here than it was for the
|
|
124
|
+
Cloudflare artifact.
|
|
125
|
+
- The gap simply *moves* unless the emitted stack is checked against the
|
|
126
|
+
project. `urlcode verify-deployment --target <url>` already probes a running
|
|
127
|
+
deployment; it would need to cover the multi-Lambda shape, or the build gains
|
|
128
|
+
a new unverified claim while retiring an honest refusal.
|
|
129
|
+
|
|
130
|
+
That last point is the one worth holding onto. The project's current position on
|
|
131
|
+
AWS is **honest**: the capability catalog reports `deployment: 'unverified'` for
|
|
132
|
+
every target that is not self-hosted, and [AWS](AWS.md) tells a reader to treat
|
|
133
|
+
the limits as unverified until they deploy the example themselves. A compiler
|
|
134
|
+
that emits infrastructure nobody has deployed would be a larger unverified claim
|
|
135
|
+
wearing the clothes of a capability.
|
|
136
|
+
|
|
137
|
+
## 5. The capability model already has the right shape
|
|
138
|
+
|
|
139
|
+
`src/capabilities.ts` (from the target-capability centralization) is where this
|
|
140
|
+
lands with no new concept:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
function / aws: refused → compiled
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`compiled` already exists as a `CapabilitySupport` value and already means what
|
|
147
|
+
is needed here — Cloudflare uses it. `deployment` stays `'unverified'` until
|
|
148
|
+
something is actually deployed. The catalog would tell the truth about the new
|
|
149
|
+
lowering without anything else in the model changing, and `urlcode capabilities
|
|
150
|
+
--target aws` would report it.
|
|
151
|
+
|
|
152
|
+
## 6. Proposed scope for a first spike
|
|
153
|
+
|
|
154
|
+
Narrow, so that the isolation story stays clean and the win is real:
|
|
155
|
+
|
|
156
|
+
**In:** `function` routes, Function URL only, one Lambda per function route,
|
|
157
|
+
a generated role per route carrying exactly that route's granted bindings,
|
|
158
|
+
and the existing native-handler Lambda unchanged for everything else.
|
|
159
|
+
|
|
160
|
+
**Out:** `middleware` (3.2), `link` (3.3), API Gateway, VPC, custom domains,
|
|
161
|
+
warm-start tuning, and any claim about cost.
|
|
162
|
+
|
|
163
|
+
**Done looks like:** a project in `examples/` that builds, a generated template
|
|
164
|
+
a reader can inspect, `urlcode capabilities --target aws` reporting `function`
|
|
165
|
+
as `compiled`, and a written comparison of the two isolation models that a
|
|
166
|
+
reviewer can disagree with.
|
|
167
|
+
|
|
168
|
+
**Not done by that:** a deployment. Everything above can pass without anyone
|
|
169
|
+
having run it on AWS, and the spike should say so rather than imply otherwise.
|
|
170
|
+
|
|
171
|
+
## 7. Prior art worth reading before building this
|
|
172
|
+
|
|
173
|
+
- **Cloudflare target in this repository** — the closest precedent, and the one
|
|
174
|
+
that establishes compile-not-adapt as a thing URLCode already does.
|
|
175
|
+
- **SST, Serverless Framework, AWS CDK** — all generate per-function
|
|
176
|
+
infrastructure from a declaration. The interesting question is not how they
|
|
177
|
+
emit it but how they keep the emitted stack honest as the provider moves.
|
|
178
|
+
- **Deno Deploy and Vercel functions** — isolate-per-request models, the closest
|
|
179
|
+
commercial thing to the guarantee QuickJS-WASM gives today.
|
|
180
|
+
|
|
181
|
+
## 8. Open questions
|
|
182
|
+
|
|
183
|
+
- Is warm-container reuse acceptable at all, given the runtime currently
|
|
184
|
+
*guarantees* fresh per-invocation state? If not, this lowering is wrong for
|
|
185
|
+
any route that relies on that guarantee, and there is no build-time way to
|
|
186
|
+
tell which ones do.
|
|
187
|
+
- Does the generated IAM role belong in URLCode's output, or should the build
|
|
188
|
+
emit a *description* of the permissions each route needs and leave the role to
|
|
189
|
+
the operator — closer to how [function security](FUNCTION-SECURITY.md) already
|
|
190
|
+
keeps grants operator-controlled and outside the checkout?
|
|
191
|
+
- Does a per-route Lambda change what `policies` can promise? `throttle` on AWS
|
|
192
|
+
is already `conditional` — "counters are per instance" — and more instances
|
|
193
|
+
make that weaker, not stronger.
|
|
194
|
+
- Is one Lambda per route the right granularity, or one per *project* with a
|
|
195
|
+
route parameter, which keeps deployment small but reintroduces a shared
|
|
196
|
+
process?
|
|
197
|
+
- What happens to the 6 MB Lambda response limit ([AWS](AWS.md)) for a function
|
|
198
|
+
route that returns a large body — refuse at build time, as the Cloudflare
|
|
199
|
+
target refuses what it cannot serve?
|