@jimhoyd/urlcode 0.4.2 → 0.4.6
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 +57 -14
- package/.claude/skills/urlcode-operations/SKILL.md +4 -0
- package/README.md +19 -15
- package/SECURITY.md +5 -3
- package/dist/BUILD-MANIFEST.json +30 -27
- package/dist/agent-context.js +82 -0
- package/dist/agents-guide.js +42 -42
- package/dist/authoring.js +12 -2
- package/dist/body-schema.js +159 -0
- package/dist/build-cloudflare.js +2 -0
- package/dist/capabilities.js +1 -1
- package/dist/cli.js +28 -12
- package/dist/config.js +74 -6
- package/dist/context.js +4 -6
- package/dist/errors.js +3 -1
- package/dist/examples.js +1 -1
- package/dist/extensions.js +60 -2
- package/dist/http-policy.js +19 -4
- package/dist/http-response.js +2 -2
- package/dist/init-with.js +71 -9
- package/dist/mcp.js +15 -2
- package/dist/pattern-guard.js +32 -0
- package/dist/policies/security.js +0 -0
- package/dist/project-tests.js +35 -11
- package/dist/readiness.js +184 -32
- package/dist/recipes.js +1 -1
- package/dist/router.js +17 -0
- package/dist/runtime.js +26 -3
- package/dist/scaffold.js +0 -0
- package/dist/server.js +26 -3
- package/dist/site.js +0 -0
- package/dist/tooling.js +2 -1
- package/dist/types/agent-context.d.ts +44 -0
- package/dist/types/authoring.d.ts +3 -1
- package/dist/types/body-schema.d.ts +58 -0
- package/dist/types/config.d.ts +10 -2
- package/dist/types/context.d.ts +1 -1
- package/dist/types/errors.d.ts +9 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/extensions.d.ts +47 -2
- package/dist/types/http-policy.d.ts +3 -0
- package/dist/types/init-with.d.ts +10 -1
- package/dist/types/pattern-guard.d.ts +10 -0
- package/dist/types/project-tests.d.ts +9 -0
- package/dist/types/readiness.d.ts +68 -0
- package/dist/types/recipes.d.ts +1 -1
- package/dist/types/runtime.d.ts +4 -0
- package/dist/types/server.d.ts +9 -1
- package/dist/types/tooling.d.ts +1 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types.js +10 -2
- package/dist/typescript-authoring.js +5 -3
- package/dist/verify-deployment.js +16 -10
- package/examples/body-validation/README.md +16 -0
- package/examples/body-validation/example.yaml +17 -0
- package/examples/body-validation/tests/requests.json +15 -0
- package/examples/body-validation/urlcode.yaml +34 -0
- package/examples/coverage-waiver/README.md +8 -0
- package/examples/coverage-waiver/example.yaml +16 -0
- package/examples/coverage-waiver/functions/notes.mjs +2 -0
- package/examples/coverage-waiver/tests/requests.json +3 -0
- package/examples/coverage-waiver/urlcode.yaml +10 -0
- package/examples/data-dir/README.md +39 -0
- package/examples/data-dir/data/welcome.txt +1 -0
- package/examples/data-dir/example.yaml +22 -0
- package/examples/data-dir/functions/note.mjs +18 -0
- package/examples/data-dir/tests/requests.json +6 -0
- package/examples/data-dir/urlcode.yaml +6 -0
- package/examples/lifecycle/README.md +15 -0
- package/examples/lifecycle/example.yaml +19 -0
- package/examples/lifecycle/functions/notes.mjs +29 -0
- package/examples/lifecycle/tests/requests.json +12 -0
- package/examples/lifecycle/urlcode.yaml +29 -0
- package/examples/not-found/README.md +10 -0
- package/examples/not-found/example.yaml +17 -0
- package/examples/not-found/public/404.html +3 -0
- package/examples/not-found/public/index.html +3 -0
- package/examples/not-found/tests/requests.json +7 -0
- package/examples/not-found/urlcode.yaml +7 -0
- package/examples/shared-blocks/README.md +11 -0
- package/examples/shared-blocks/example.yaml +17 -0
- package/examples/shared-blocks/tests/requests.json +7 -0
- package/examples/shared-blocks/urlcode.yaml +37 -0
- package/llms-full.txt +364 -49
- package/llms.txt +43 -5
- package/package.json +15 -22
- package/recipes/static-page/README.md +9 -0
- package/recipes/static-page/public/index.html +11 -0
- package/recipes/static-page/recipe.yaml +21 -0
- package/recipes/static-page/tests/requests.json +22 -0
- package/recipes/static-page/urlcode.yaml +7 -0
- package/recipes/static-plus-api/README.md +6 -0
- package/recipes/static-plus-api/urlcode.yaml +4 -0
- package/recipes/store-crud/README.md +53 -0
- package/recipes/store-crud/recipe.yaml +31 -0
- package/recipes/store-crud/tests/requests.json +18 -0
- package/recipes/store-crud/urlcode.yaml +18 -0
- package/schemas/urlcode.schema.json +125 -60
- package/skills/urlcode/SKILL.md +47 -27
- package/starters/default/AGENTS.md +43 -43
- package/starters/page/README.md +14 -0
- package/starters/page/public/index.html +12 -0
- package/starters/page/tests/requests.json +17 -0
- package/starters/page/urlcode.yaml +6 -0
- package/.claude-plugin/marketplace.json +0 -18
- package/CONTRIBUTING.md +0 -148
- package/ROADMAP.md +0 -70
- package/docs/AI-AUTHORING.md +0 -339
- package/docs/ASSETS.md +0 -107
- package/docs/AUTH-BACKUP.md +0 -32
- package/docs/AWS.md +0 -86
- package/docs/BEST-PRACTICES.md +0 -276
- package/docs/BULK.md +0 -79
- package/docs/CAPABILITIES.md +0 -192
- package/docs/CAPACITY.md +0 -305
- package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
- package/docs/CI.md +0 -147
- package/docs/CLOUDFLARE.md +0 -109
- package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
- package/docs/COMPLIANCE.md +0 -239
- package/docs/COMPOSING-A-SITE.md +0 -287
- package/docs/CONDITIONS.md +0 -74
- package/docs/CONTAINER-PROMOTION.md +0 -74
- package/docs/DEPLOYMENT-CHECKS.md +0 -108
- package/docs/DEVELOPMENT-PIPELINE.md +0 -304
- package/docs/EGRESS.md +0 -125
- package/docs/EXTENSIONS.md +0 -403
- package/docs/FRAMEWORK.md +0 -217
- package/docs/FUNCTION-SECURITY.md +0 -251
- package/docs/HTTP.md +0 -129
- package/docs/INSTALL.md +0 -128
- package/docs/INTERCHANGE.md +0 -134
- package/docs/LOAD-TESTING.md +0 -91
- package/docs/LOCAL-DEVELOPMENT.md +0 -102
- package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
- package/docs/MIDDLEWARE.md +0 -102
- package/docs/MONITORING.md +0 -115
- package/docs/OBSERVABILITY.md +0 -222
- package/docs/OPEN-DECISIONS.md +0 -149
- package/docs/OPERATIONAL-PROOF.md +0 -41
- package/docs/OPERATIONS.md +0 -201
- package/docs/ORGANIZATION.md +0 -135
- package/docs/PERFORMANCE.md +0 -72
- package/docs/PLUGINS.md +0 -271
- package/docs/POLICIES.md +0 -211
- package/docs/PRERENDER.md +0 -245
- package/docs/PROJECT-DIRECTION.md +0 -118
- package/docs/PROVIDER-VERIFICATION.md +0 -84
- package/docs/READINESS.md +0 -153
- package/docs/README.md +0 -93
- package/docs/RECIPES.md +0 -99
- package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
- package/docs/RELEASE-0.4.1.md +0 -73
- package/docs/RELEASE-0.4.2.md +0 -30
- package/docs/RELEASE-READINESS.md +0 -146
- package/docs/RELEASE-SECURITY.md +0 -100
- package/docs/RESILIENCE.md +0 -161
- package/docs/ROUTING.md +0 -92
- package/docs/SANDBOX-REVIEW.md +0 -72
- package/docs/SCAFFOLDING.md +0 -70
- package/docs/SECURITY-AUDIT.md +0 -164
- package/docs/SITE.md +0 -150
- package/docs/SPECIFICATION.md +0 -359
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
- package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
- package/docs/SPIKE-CORE-LAYERING.md +0 -368
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
- package/docs/STANDARDS.md +0 -311
- package/docs/STARTERS.md +0 -83
- package/docs/STATIC.md +0 -105
- package/docs/TOOLING.md +0 -298
- package/docs/TUNNELS.md +0 -72
- package/docs/TYPESCRIPT-AUTHORING.md +0 -87
- package/docs/TYPESCRIPT.md +0 -123
- package/docs/VERCEL.md +0 -114
- package/docs/VERSION-ALIGNMENT.md +0 -88
- package/docs/YAML-GUIDE.md +0 -57
- package/docs/YAML-REFERENCE.md +0 -449
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
- package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
- package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
- package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
- package/docs/archive/2026-09-19/ROADMAP.md +0 -387
- package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
- package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
- package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
- package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
- package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
- package/docs/archive/README.md +0 -29
- package/docs/policies/agents.md +0 -182
- package/docs/policies/cache.md +0 -152
- package/docs/policies/compression.md +0 -169
- package/docs/policies/contract.md +0 -52
- package/docs/policies/hardened.md +0 -56
- package/docs/policies/interoperability.md +0 -169
- package/docs/policies/operations.md +0 -45
- package/docs/policies/security.md +0 -161
- package/docs/policies/throttle.md +0 -103
- package/docs/yaml/assets.md +0 -36
- package/docs/yaml/conditions.md +0 -20
- package/docs/yaml/functions.md +0 -168
- package/docs/yaml/middleware.md +0 -31
- package/docs/yaml/organization.md +0 -74
- package/docs/yaml/policies.md +0 -37
- package/docs/yaml/redirects.md +0 -64
- package/docs/yaml/responses.md +0 -57
- package/docs/yaml/site.md +0 -24
- package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
- 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.
|