@jimhoyd/urlcode 0.4.2 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +42 -18
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +30 -27
  6. package/dist/agent-context.js +82 -0
  7. package/dist/agents-guide.js +42 -42
  8. package/dist/authoring.js +12 -2
  9. package/dist/body-schema.js +159 -0
  10. package/dist/build-cloudflare.js +2 -0
  11. package/dist/capabilities.js +1 -1
  12. package/dist/cli.js +28 -12
  13. package/dist/config.js +74 -6
  14. package/dist/context.js +4 -6
  15. package/dist/errors.js +3 -1
  16. package/dist/examples.js +1 -1
  17. package/dist/extensions.js +60 -2
  18. package/dist/http-policy.js +19 -4
  19. package/dist/http-response.js +2 -2
  20. package/dist/init-with.js +71 -9
  21. package/dist/mcp.js +15 -2
  22. package/dist/pattern-guard.js +32 -0
  23. package/dist/policies/security.js +0 -0
  24. package/dist/project-tests.js +35 -11
  25. package/dist/readiness.js +184 -32
  26. package/dist/recipes.js +1 -1
  27. package/dist/router.js +17 -0
  28. package/dist/runtime.js +26 -3
  29. package/dist/scaffold.js +0 -0
  30. package/dist/server.js +26 -3
  31. package/dist/site.js +0 -0
  32. package/dist/tooling.js +2 -1
  33. package/dist/types/agent-context.d.ts +44 -0
  34. package/dist/types/authoring.d.ts +3 -1
  35. package/dist/types/body-schema.d.ts +58 -0
  36. package/dist/types/config.d.ts +10 -2
  37. package/dist/types/context.d.ts +1 -1
  38. package/dist/types/errors.d.ts +9 -1
  39. package/dist/types/examples.d.ts +1 -1
  40. package/dist/types/extensions.d.ts +47 -2
  41. package/dist/types/http-policy.d.ts +3 -0
  42. package/dist/types/init-with.d.ts +10 -1
  43. package/dist/types/pattern-guard.d.ts +10 -0
  44. package/dist/types/project-tests.d.ts +9 -0
  45. package/dist/types/readiness.d.ts +68 -0
  46. package/dist/types/recipes.d.ts +1 -1
  47. package/dist/types/runtime.d.ts +4 -0
  48. package/dist/types/server.d.ts +9 -1
  49. package/dist/types/tooling.d.ts +1 -0
  50. package/dist/types/types.d.ts +13 -0
  51. package/dist/types.js +10 -2
  52. package/dist/typescript-authoring.js +5 -3
  53. package/dist/verify-deployment.js +16 -10
  54. package/examples/body-validation/README.md +16 -0
  55. package/examples/body-validation/example.yaml +17 -0
  56. package/examples/body-validation/tests/requests.json +15 -0
  57. package/examples/body-validation/urlcode.yaml +34 -0
  58. package/examples/coverage-waiver/README.md +8 -0
  59. package/examples/coverage-waiver/example.yaml +16 -0
  60. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  61. package/examples/coverage-waiver/tests/requests.json +3 -0
  62. package/examples/coverage-waiver/urlcode.yaml +10 -0
  63. package/examples/data-dir/README.md +39 -0
  64. package/examples/data-dir/data/welcome.txt +1 -0
  65. package/examples/data-dir/example.yaml +22 -0
  66. package/examples/data-dir/functions/note.mjs +18 -0
  67. package/examples/data-dir/tests/requests.json +6 -0
  68. package/examples/data-dir/urlcode.yaml +6 -0
  69. package/examples/lifecycle/README.md +15 -0
  70. package/examples/lifecycle/example.yaml +19 -0
  71. package/examples/lifecycle/functions/notes.mjs +29 -0
  72. package/examples/lifecycle/tests/requests.json +12 -0
  73. package/examples/lifecycle/urlcode.yaml +29 -0
  74. package/examples/not-found/README.md +10 -0
  75. package/examples/not-found/example.yaml +17 -0
  76. package/examples/not-found/public/404.html +3 -0
  77. package/examples/not-found/public/index.html +3 -0
  78. package/examples/not-found/tests/requests.json +7 -0
  79. package/examples/not-found/urlcode.yaml +7 -0
  80. package/examples/shared-blocks/README.md +11 -0
  81. package/examples/shared-blocks/example.yaml +17 -0
  82. package/examples/shared-blocks/tests/requests.json +7 -0
  83. package/examples/shared-blocks/urlcode.yaml +37 -0
  84. package/llms-full.txt +364 -49
  85. package/llms.txt +44 -6
  86. package/package.json +15 -26
  87. package/recipes/static-page/README.md +9 -0
  88. package/recipes/static-page/public/index.html +11 -0
  89. package/recipes/static-page/recipe.yaml +21 -0
  90. package/recipes/static-page/tests/requests.json +22 -0
  91. package/recipes/static-page/urlcode.yaml +7 -0
  92. package/recipes/static-plus-api/README.md +6 -0
  93. package/recipes/static-plus-api/urlcode.yaml +4 -0
  94. package/recipes/store-crud/README.md +53 -0
  95. package/recipes/store-crud/recipe.yaml +31 -0
  96. package/recipes/store-crud/tests/requests.json +18 -0
  97. package/recipes/store-crud/urlcode.yaml +18 -0
  98. package/schemas/urlcode.schema.json +125 -60
  99. package/skills/urlcode/SKILL.md +47 -27
  100. package/starters/default/AGENTS.md +43 -43
  101. package/starters/page/README.md +14 -0
  102. package/starters/page/public/index.html +12 -0
  103. package/starters/page/tests/requests.json +17 -0
  104. package/starters/page/urlcode.yaml +6 -0
  105. package/.claude-plugin/marketplace.json +0 -18
  106. package/CONTRIBUTING.md +0 -148
  107. package/ROADMAP.md +0 -70
  108. package/docs/AI-AUTHORING.md +0 -339
  109. package/docs/ASSETS.md +0 -107
  110. package/docs/AUTH-BACKUP.md +0 -32
  111. package/docs/AWS.md +0 -86
  112. package/docs/BEST-PRACTICES.md +0 -276
  113. package/docs/BULK.md +0 -79
  114. package/docs/CAPABILITIES.md +0 -192
  115. package/docs/CAPACITY.md +0 -305
  116. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  117. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  118. package/docs/CI.md +0 -147
  119. package/docs/CLOUDFLARE.md +0 -109
  120. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  121. package/docs/COMPLIANCE.md +0 -239
  122. package/docs/COMPOSING-A-SITE.md +0 -287
  123. package/docs/CONDITIONS.md +0 -74
  124. package/docs/CONTAINER-PROMOTION.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -304
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -403
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -251
  131. package/docs/HTTP.md +0 -129
  132. package/docs/INSTALL.md +0 -128
  133. package/docs/INTERCHANGE.md +0 -134
  134. package/docs/LOAD-TESTING.md +0 -91
  135. package/docs/LOCAL-DEVELOPMENT.md +0 -102
  136. package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
  137. package/docs/MIDDLEWARE.md +0 -102
  138. package/docs/MONITORING.md +0 -115
  139. package/docs/OBSERVABILITY.md +0 -222
  140. package/docs/OPEN-DECISIONS.md +0 -149
  141. package/docs/OPERATIONAL-PROOF.md +0 -41
  142. package/docs/OPERATIONS.md +0 -201
  143. package/docs/ORGANIZATION.md +0 -135
  144. package/docs/PERFORMANCE.md +0 -72
  145. package/docs/PLUGINS.md +0 -271
  146. package/docs/POLICIES.md +0 -211
  147. package/docs/PRERENDER.md +0 -245
  148. package/docs/PROJECT-DIRECTION.md +0 -118
  149. package/docs/PROVIDER-VERIFICATION.md +0 -84
  150. package/docs/READINESS.md +0 -153
  151. package/docs/README.md +0 -93
  152. package/docs/RECIPES.md +0 -99
  153. package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
  154. package/docs/RELEASE-0.4.1.md +0 -73
  155. package/docs/RELEASE-0.4.2.md +0 -30
  156. package/docs/RELEASE-READINESS.md +0 -146
  157. package/docs/RELEASE-SECURITY.md +0 -100
  158. package/docs/RESILIENCE.md +0 -161
  159. package/docs/ROUTING.md +0 -92
  160. package/docs/SANDBOX-REVIEW.md +0 -72
  161. package/docs/SCAFFOLDING.md +0 -70
  162. package/docs/SECURITY-AUDIT.md +0 -164
  163. package/docs/SITE.md +0 -150
  164. package/docs/SPECIFICATION.md +0 -359
  165. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  166. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  167. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  168. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
  169. package/docs/STANDARDS.md +0 -311
  170. package/docs/STARTERS.md +0 -83
  171. package/docs/STATIC.md +0 -105
  172. package/docs/TOOLING.md +0 -298
  173. package/docs/TUNNELS.md +0 -72
  174. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  175. package/docs/TYPESCRIPT.md +0 -123
  176. package/docs/VERCEL.md +0 -114
  177. package/docs/VERSION-ALIGNMENT.md +0 -88
  178. package/docs/YAML-GUIDE.md +0 -57
  179. package/docs/YAML-REFERENCE.md +0 -449
  180. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  181. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  182. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  183. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  184. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  185. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  186. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  188. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  189. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  190. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  191. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  192. package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
  193. package/docs/archive/README.md +0 -29
  194. package/docs/policies/agents.md +0 -182
  195. package/docs/policies/cache.md +0 -152
  196. package/docs/policies/compression.md +0 -169
  197. package/docs/policies/contract.md +0 -52
  198. package/docs/policies/hardened.md +0 -56
  199. package/docs/policies/interoperability.md +0 -169
  200. package/docs/policies/operations.md +0 -45
  201. package/docs/policies/security.md +0 -161
  202. package/docs/policies/throttle.md +0 -103
  203. package/docs/yaml/assets.md +0 -36
  204. package/docs/yaml/conditions.md +0 -20
  205. package/docs/yaml/functions.md +0 -168
  206. package/docs/yaml/middleware.md +0 -31
  207. package/docs/yaml/organization.md +0 -74
  208. package/docs/yaml/policies.md +0 -37
  209. package/docs/yaml/redirects.md +0 -64
  210. package/docs/yaml/responses.md +0 -57
  211. package/docs/yaml/site.md +0 -24
  212. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  213. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
  214. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
@@ -1,430 +0,0 @@
1
- # Historical record
2
-
3
- Archived 2026-09-19. This records an earlier implementation or proposal, not
4
- current instructions. See the [current roadmap](../../../ROADMAP.md),
5
- [current contract](../../SPECIFICATION.md) and [open decisions](../../OPEN-DECISIONS.md).
6
- Remaining acceptance work is not declared complete by archiving this record.
7
-
8
- <!-- trust-model-prose: historical-file -->
9
- <!-- local-links: historical-file -->
10
- <!-- guidance-claims: ignore-file -->
11
-
12
- # Review: the extension model, its precedents and its alignment
13
-
14
- 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
15
- [UI kit](https://github.com/jimhoyd-com/urlcode-ui/blob/main/docs/SPIKE-UI.md) spikes against the runtime's principles and against
16
- how established frameworks add the same capabilities. Core portability is
17
- the fixed point: a project's YAML must work unchanged on another host.
18
- Section 1 is what the review changed; section 2 is what other projects do
19
- and what was taken from each; section 3 is the case for the model.
20
-
21
- ## 1. Alignment issues found and how they were resolved
22
-
23
- 1. **A YAML-only project has no server file.** The spikes said `init`
24
- writes "a plugin line into the server file", but projects run through
25
- `urlcode dev` and `urlcode serve`; the starter is YAML, functions and a
26
- Makefile. Operator material already loads from explicit paths
27
- (`--policy`, `--compliance-rules`, `--auth-file`), never from inside the
28
- project. Resolved: a host file, `host.js`, exporting `{ store, plugins }`,
29
- loaded with `--host-file <path>` and written by `init`; the starter's
30
- `make dev` and `make serve` pass it. It is the one file of code an
31
- extended project has, it is operator code by the same rule as the grant
32
- file, and it is never discovered by convention inside the project
33
- because the project is untrusted content.
34
- 2. **`origin` in `auth.yaml` broke portability.** The passkey relying-party
35
- id and provider redirect base were in the auth block. An origin is a
36
- deployment fact; the runtime already takes `--origin` for the sitemap
37
- and compliance. Resolved: removed from YAML, taken from `--origin`.
38
- 3. **`mount` duplicated the route.** The auth block carried `mount:
39
- /account` while the route `/account/*: { extension: auth }` already
40
- said where the extension lives. Resolved: the route is the mount.
41
- 4. **Two ways to protect a route.** `protect` path patterns in `auth.yaml`
42
- and `policies.auth` on routes would drift apart. Policies already have
43
- profiles for sharing a requirement across routes. Resolved: `protect`
44
- removed; `policies.auth` on a route or in a profile is the only way.
45
- 5. **`extensions.ui` had no owner.** The runtime seam refuses a block no
46
- plugin claims, and the kit was described as a library. Resolved: the kit
47
- ships one small plugin that owns the block and serves the stylesheet and
48
- scripts at one route, which also answers how two extensions avoid
49
- serving the same asset twice.
50
- 6. **Extension routes had no fixtures.** The audit generates fixtures for
51
- native routes from their declarations; it cannot for an extension route.
52
- Resolved: the plugin supplies fixtures for its routes through the
53
- existing `testPlan` seam, so the audit, `urlcode test` and
54
- `verify-deployment` cover them.
55
- 7. **Translations were "later".** Every string in the kit and the notices
56
- is catalogued from the first release, with language negotiation, plural
57
- rules, locale formatting and RTL-safe templates; English ships, any
58
- language is a file. Adding this later would have meant re-touching every
59
- template.
60
- 8. **`__Host-` cookies in development.** The prefix requires `Secure`;
61
- browsers treat `http://localhost` as a secure context so it works there,
62
- but not on a LAN address. `urlcode dev` uses a `__Host-` cookie on
63
- localhost and a plain-named one elsewhere, and `hardened` refuses
64
- anything but `__Host-` in production. Noted in the auth spike.
65
-
66
- What did not need changing: the four runtime seams stay generic; the
67
- store contract stays a document store with declared indexes and no joins;
68
- the plugin remains the only host-code seam; nothing in YAML names a
69
- package; every target either enforces or refuses at activation; the
70
- runtime never depends on an extension.
71
-
72
- ## 2. How other frameworks do this, and what was taken
73
-
74
- The pattern "core stays small, capabilities arrive as installable pieces
75
- that bring routes, storage, pages and configuration, and the app overrides
76
- them by file" is thirty years old. The instructive precedents:
77
-
78
- | Framework | The piece | Adds routes | Adds storage | Pages and override | Configuration | Taken |
79
- |---|---|---|---|---|---|---|
80
- | **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 |
81
- | **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 |
82
- | **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 |
83
- | **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 |
84
- | **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 |
85
- | **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 |
86
- | **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 |
87
- | **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 |
88
- | **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 |
89
-
90
- Two observations from the table shape the model:
91
-
92
- - **Every mature framework ended up with the same four things**: a way to
93
- include routes, a way to add storage with migrations, a way to override
94
- pages by file, and a configuration surface per piece. The runtime's four
95
- seams are those four things, named for this runtime.
96
- - **The frameworks that ship a UI with the capability (Django admin,
97
- Devise views, Breeze, Filament, Keycloak) are the ones people reach for
98
- first**, and the ones that leave UI to the app (Kratos, Supabase, Auth.js)
99
- are the ones where every project rebuilds the same sign-in page. The kit
100
- and the accounts page are the difference.
101
-
102
- Where this model departs from all of them, deliberately: the
103
- configuration is YAML validated by a schema rather than code, so a project
104
- is data that can be checked, diffed, audited and moved; the extension
105
- brings no framework of its own to the browser; and the trust boundary
106
- between the project (untrusted) and the operator (host code) is explicit,
107
- which none of the precedents have because they assume the app author and
108
- the operator are the same person.
109
-
110
- ## 3. Does this let a builder, human or AI, build less?
111
-
112
- Yes, with conditions. The claim is not that a framework makes building
113
- faster in general; it is that this shape removes the specific work that is
114
- both repetitive and dangerous to get wrong.
115
-
116
- What it removes:
117
-
118
- - **Auth, accounts and admin are the parts every product needs and the
119
- parts most often built badly.** Password storage, session fixation,
120
- enumeration, CSRF, open redirects, recovery abuse: an AI writing these
121
- from scratch reproduces the average of what it has read, and the average
122
- is not good. A declared method with the decisions already made is safer
123
- than a well-prompted rewrite.
124
- - **Declaration is a smaller target than code.** A schema-validated YAML
125
- key has a few valid values; a hand-written sign-in flow has unbounded
126
- ways to be subtly wrong. Generation against a schema, with `validate`,
127
- `audit` and `test` as the loop, is where an AI is reliable. The runtime
128
- already leans this way (`llms.txt`, the YAML reference, executable
129
- examples with fixtures).
130
- - **Overrides by file, not by fork.** Restyling by theme variables and
131
- ejecting one template is work an AI does well and cannot break the flow
132
- with, because the template cannot change behaviour.
133
- - **Portability means the work survives the next decision.** A project
134
- that moves from a laptop to a server to a Worker keeps its YAML; the
135
- builder does not redo the product to change hosts.
136
-
137
- The conditions, without which the claim fails:
138
-
139
- - **Coverage.** The extensions must cover what a typical product needs;
140
- if the first thing a builder wants is missing, they are back to building.
141
- The scope cut in the auth spike is a bet that passwords, passkeys, email
142
- codes, roles, an accounts page and an admin cover the first ten
143
- products. That bet should be checked against the first three real ones.
144
- - **The escape hatch must be as easy as the declaration.** The twenty
145
- percent that is product-specific goes into functions in the WASM guest
146
- with granted bindings. If that path is harder than declaring, builders
147
- will route around the runtime.
148
- - **Documentation written for a reader with no history.** An AI has read
149
- a great deal of Django and Rails and nothing of this runtime. The
150
- schema, the reference, the cookbook and `llms.txt` are the training
151
- data; every extension needs the same set on day one, and `init` must
152
- print what it did in words a first-time reader can follow.
153
- - **Small surface, stable contracts.** The advantage disappears if the
154
- YAML keys churn. View-model versioning, the seam contracts and the store
155
- contract are the promises; they should change rarely and loudly.
156
-
157
- The honest comparison is not "this framework versus writing it by hand".
158
- It is "this framework versus Clerk, Supabase or Firebase", which also let
159
- a builder skip auth. Those win on time to first sign-in today and lose on
160
- portability, on cost at scale, on data ownership and on running offline or
161
- on a Worker. This model's claim to be better for a builder is that it
162
- offers the same skip with the YAML, the data and the pages staying theirs.
163
- That is a real position, and it holds only while the runtime stays as
164
- portable as it is now.
165
-
166
- ## 4. Against Clerk
167
-
168
- Clerk is the product a developer reaches for when they want auth, an
169
- accounts page and organizations without building any of it, so it is the
170
- right yardstick. Feature by feature, with Clerk as of 2026:
171
-
172
- | Clerk has | This model, first release | Gap |
173
- |---|---|---|
174
- | 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 |
175
- | Hosted account portal | Self-hosted accounts page at `/account` | None: self-hosted is the point |
176
- | 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 |
177
- | MFA: TOTP, SMS, backup codes | TOTP, passkey, recovery codes | SMS as a factor (deliberately) |
178
- | Multi-session: several accounts signed in, switch between them | One session per browser | Account switching. Small to add later; the session model allows it |
179
- | 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 |
180
- | 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 |
181
- | User metadata: public, private, unsafe per user | Added below as `metadata` on the account with the same three visibilities | |
182
- | Impersonation, dashboard with analytics, user management UI | Admin extension: dashboard and full user management | Same shape, ships one release later |
183
- | Webhooks (Svix) for every event | Observability events and host hooks | A webhook sender with signing and retries; added to the later list |
184
- | 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 |
185
- | 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 |
186
- | Localization: many languages shipped | Mechanism day one; English shipped | Translated catalogues. Community and native review needed |
187
- | Theming: appearance prop, CSS variables, themes | Theme variables, ejected templates, own stylesheet | None |
188
- | Testing tokens, test mode | Test mode with deterministic codes and a fake identity provider | None |
189
- | 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 |
190
- | 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 |
191
-
192
- Added to the first release from this comparison, because each is small
193
- and each is something a builder would notice missing on day one:
194
-
195
- - **Any OpenID Connect provider** by issuer URL and client id, with
196
- discovery, beside the named Google and Apple: `oidc: { okta: { issuer:
197
- … } }`. Named providers are sugar over this.
198
- - **Account `metadata`** with `public`, `private` and `unsafe` scopes:
199
- public is readable by the guest binding and the accounts page, private
200
- only by the host and admin, unsafe writable by the user. Declared
201
- fields with types in YAML, so it is still a schema.
202
- - **Sign-up controls**: `registration: open | invite-only | waitlist |
203
- off`, with an allowlist and blocklist of emails and domains and the
204
- bundled disposable-domain list.
205
- - **A Turnstile adapter** for the `challenge` hook (and the hook stays
206
- vendor-neutral; hCaptcha and reCAPTCHA adapters are a few lines each).
207
-
208
- Moved onto the later list: multi-session account switching, a signed
209
- webhook sender with retries, token issuance for third-party services, and
210
- the React component package.
211
-
212
- ### Will it be the obvious choice?
213
-
214
- Not for everyone, and it should not try to be. It becomes the obvious
215
- choice for a specific developer, and that developer is common:
216
-
217
- - Someone who wants to own the data and the pages, run on their own host
218
- or a Worker, and never pay per user. Clerk's pricing and hosted portal
219
- are the reasons people leave it at scale.
220
- - Someone building with an AI, or as a small team, who wants the whole
221
- product declared and checked rather than assembled from SDK calls. The
222
- YAML, the audit and the fixtures are the pitch; Clerk has no equivalent
223
- of "diff this pull request's route and policy changes".
224
- - Someone in a regulated or data-residency context who needs the
225
- evidence, not a vendor's certificate.
226
- - Someone whose site started as redirects and pages and is adding
227
- accounts, which is exactly the runtime's on-ramp.
228
-
229
- It is not the obvious choice, today, for a React or Next.js team that
230
- wants drop-in components and organizations with SAML this quarter, or for
231
- a team that wants a vendor to hold the compliance paper. Both are
232
- reachable: organizations and SSO are the planned second phase, and the
233
- React package is a cut, not a rejection.
234
-
235
- What decides it in practice is not the feature table. It is whether a
236
- developer gets from `npm install` to a working, good-looking sign-in with
237
- passkeys in under five minutes, whether the docs answer the next question
238
- before it is asked, and whether the first three real products fit the
239
- scope. Those three are the work.
240
-
241
- ## 5. Stepping back: is this still one system, and do the extensions make sense?
242
-
243
- With auth, admin, the kit and the candidates after them, the runtime stops
244
- being "a portable URL runtime" and becomes a declarative web application
245
- platform: a small kernel and a set of installable capabilities. That is a
246
- change of identity and it should be said out loud rather than drift. The
247
- question is whether the extension shape is the right way to become that,
248
- against the two alternatives.
249
-
250
- - **Everything in core**, the Django `contrib` way. Fastest to build and
251
- the most coherent to document, but every site would carry auth code it
252
- does not use, the Cloudflare closure would grow, and the runtime's
253
- promise that a redirect-only project is tiny and portable would erode.
254
- Rejected.
255
- - **Separate services**, the Keycloak way: auth as its own server the
256
- site talks to. Cleanest isolation, but a second process to run, a
257
- second store, and the accounts page lives somewhere else. It is what
258
- people leave Keycloak to avoid. Rejected.
259
- - **Extensions on generic seams**, the Rails engine and Laravel package
260
- way. Core stays a kernel; a capability is a package that brings routes,
261
- collections, pages and a YAML block; the operator installs it. This is
262
- what every long-lived framework converged on, and it is the shape the
263
- spikes take.
264
-
265
- So yes, the extensions make sense, on four conditions that the review
266
- adds to the plan:
267
-
268
- 1. **Name the whole.** The runtime is the kernel; the kit and the
269
- extensions are the distribution. The README should say "a portable
270
- runtime for sites and the accounts, admin and forms they grow into",
271
- and the roadmap should show the path in section 1 of the kit spike.
272
- The principles do not change; the pitch does.
273
- 2. **Keep customisation in the untrusted tier.** Extensions are trusted
274
- host code; a project's own logic is untrusted WASM. <!-- trust-model-prose: historical -->
275
- A builder who wants a custom rule in a flow ("only `@acme.com` may register",
276
- "after sign-up, create a workspace") must not have to write host
277
- code. Extension lifecycle hooks should be able to call a project
278
- function in the guest, through a granted binding, with a typed input
279
- and output. Customisation then stays portable YAML plus a guest
280
- function, and the host file stays what `init` wrote.
281
- 3. **The store needs aggregates.** A document store with equality
282
- lookups serves auth, but the admin dashboard and every product feature
283
- want counts and time buckets. Add `count(where)` and a bucketed count
284
- by a declared timestamp index to the contract now, so no extension is
285
- tempted to open the backend directly.
286
- 4. **The Node-free rule needs tooling.** Requiring extension cores to be
287
- free of Node imports is the price of every target working. It is
288
- only bearable if the kit ships the closure check and a scaffold
289
- (`create-urlcode-extension`) that starts an extension in the right
290
- shape, so third parties can add extensions on the same seams without
291
- reading the runtime's source.
292
-
293
- One tension remains and should stay visible: two tiers of trust. An
294
- operator who installs an extension trusts it completely; a project author
295
- is trusted with nothing. That is the browser's model (extensions versus
296
- pages) and it is right for a runtime that hosts other people's YAML, but
297
- it means the extension repositories carry the security burden of the
298
- whole system. The review before 1.0, the threat models and the dependency
299
- policy in the auth spike are that burden made explicit.
300
-
301
- ## 6. How to make it better than the alternatives
302
-
303
- The feature table in section 4 is the floor. What makes it the choice is
304
- below, ordered by leverage.
305
-
306
- 1. **Five minutes to a passkey sign-in, visibly.** `npm create urlcode`
307
- asks three questions and produces a site with auth on; a public demo
308
- runs the cookbook with the accounts page; the README's first screen is
309
- that demo. Measure the time and print it in the docs.
310
- 2. **Import from where people are.** Importers for Clerk, Supabase,
311
- Auth.js and Firebase user exports, including verifying their password
312
- hashes (bcrypt and PBKDF2 alongside scrypt and Argon2id, recorded per
313
- hash and upgraded on sign-in). Nobody switches auth if their users
314
- must reset passwords.
315
- 3. **Built for the AI that builds with it.** A `llms.txt` per package,
316
- the schema published to SchemaStore for editor completion, errors that
317
- name the YAML key and the fix, and an MCP server that exposes
318
- `validate`, `audit`, `test`, `routes --compare` and `doctor` so an
319
- agent can check its own work before a human sees it. The runtime's
320
- fixtures and audit already make a project checkable; this makes it
321
- checkable from inside the tools people build with.
322
- 4. **Extension authoring for third parties.** The scaffold, the closure
323
- check, the seam contracts as published types, and one worked example
324
- (`forms`) small enough to read in an hour. A platform with two
325
- first-party extensions is a product; one with twenty third-party ones
326
- is an ecosystem.
327
- 5. **Starters that are products.** A links site, a docs site, a
328
- members-only site and a small SaaS skeleton, each a YAML project with
329
- fixtures, each the answer to "what does this look like finished".
330
- 6. **Trust made public.** The threat models, the independent review's
331
- report, the release provenance and the benchmarks published, not
332
- summarised. This is the answer to "why not a vendor".
333
- 7. **The edge story finished.** Cloudflare with the D1 backend and the
334
- `--extension` build is the deployment nobody else offers for a full
335
- accounts system in a Worker; it should be the second target, not the
336
- fourth.
337
- 8. **Operations that a small team can run.** One store, one export, one
338
- restore drill, `doctor` for every target, the breach-response
339
- commands, and the compliance evidence export: the argument that
340
- self-hosting is not a burden is that these exist.
341
-
342
- ## 7. Forkable by design
343
-
344
- Apache-2.0 makes forking legal. The design has to make it practical: a
345
- team should be able to take `urlcode-auth`, change what they disagree
346
- with, publish `acme-auth`, and have every project that says `extension:
347
- auth` work with it unchanged. That is only possible if the *name* in the
348
- YAML is a contract and the package is one implementation of it.
349
-
350
- **The contract is separate from the implementation.** For each extension
351
- kind there is a small contract package, owned by the runtime's
352
- organisation, versioned by semver, with no code that does anything:
353
-
354
- ```
355
- @jimhoyd/urlcode-auth-contract
356
- schema/ the JSON schema for extensions.auth and for policies.auth
357
- routes.md the routes an implementation must serve under its mount and what each returns
358
- collections/ the store collections, keys and indexes an implementation uses
359
- view-models/ the typed view model of every page, versioned
360
- copy/ the catalogue ids and the English strings
361
- fixtures/ request fixtures every implementation must pass
362
- conformance/ a test suite that runs against any implementation
363
- ```
364
-
365
- The original and every fork depend on the contract, never on each other.
366
- A fork that keeps the contract is a drop-in: same YAML, same templates,
367
- same translations, same admin extension on top. A fork that changes the
368
- contract picks a new name (`extension: acme-auth`) and its own contract
369
- package, and is honest about not being a drop-in. The runtime's
370
- `extensions` seam only cares that exactly one plugin claims a name.
371
-
372
- **What the runtime provides so forks need nothing private:**
373
-
374
- - The seam types (`Plugin`, `PolicyModule`, the store contract, the
375
- context bag, the fixtures shape) as published declarations from
376
- `@jimhoyd/urlcode`, with semver and a deprecation window. Nothing an
377
- extension needs is reachable only through an unexported path.
378
- - `provides: 'auth'` in a plugin's registration, distinct from the
379
- package name, so `admin` requires "a plugin providing `auth` at
380
- contract `^1`", not `@jimhoyd/urlcode-auth`.
381
- - The conformance runner: `urlcode extension conform --contract
382
- @jimhoyd/urlcode-auth-contract --host-file host.js` runs the fixtures
383
- and the conformance suite against whatever is installed.
384
-
385
- **What each extension repository does so a fork is an afternoon, not a
386
- month:**
387
-
388
- - One package per repository, the runtime's own CI workflows, release
389
- scripts and container build copied rather than referenced, so the fork
390
- builds and releases on its own the day it is created.
391
- - No product name, colour or URL in code or templates; everything comes
392
- from the theme block and the catalogue, so a fork is not full of the
393
- original's branding.
394
- - No telemetry, no update check, no call home. A fork has nothing to
395
- remove.
396
- - `FORKING.md` at the root: what to rename, which contract version the
397
- code implements, how to run conformance, how to publish under a scope,
398
- and the trademark rule: the runtime's name is not granted by the
399
- licence, so a fork is `acme-auth`, not `urlcode-auth-acme`, while
400
- `provides: 'auth'` stays.
401
- - The scaffold, `create-urlcode-extension`, creates a new extension in
402
- the same shape, and `--from @jimhoyd/urlcode-auth` creates a fork with
403
- the renames done.
404
- - Contract changes are proposals on the contract repository, not commits
405
- to an implementation, and the original implementation has no special
406
- standing there beyond being first.
407
-
408
- **Why this is worth the extra package.** It is the same discipline the
409
- runtime applies to YAML: behaviour is declared in a portable document and
410
- any conforming host runs it. Applied to extensions, the portable document
411
- is the contract and any conforming implementation serves it. It also
412
- keeps the original honest: if the contract is good enough to fork
413
- against, it is good enough to build against.
414
-
415
- ## 8. The work, by repository
416
-
417
- What sections 5 through 7 add, placed where it belongs. Nothing here is
418
- started.
419
-
420
- | Repository | Adds |
421
- |---|---|
422
- | `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 |
423
- | `urlcode-ui` | The kit; the closure check as a reusable test; `create-urlcode-extension` with `--from`; the worked `forms` example |
424
- | `urlcode-auth-contract`, `urlcode-admin-contract`, `urlcode-ui-contract` | Schema, routes, collections, view models, copy ids, fixtures, conformance |
425
- | `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 |
426
- | `urlcode-admin` | Dashboard and users as specified; requires a provider of `auth`, not a package; `FORKING.md` |
427
-
428
- Cloudflare moves to the second target after `node`, before Vercel and
429
- AWS, because it is the deployment no alternative offers for a full
430
- accounts system.