clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

main
lofi / docs / decisions / nested-namespaces-alpha53.md
nested-namespaces-alpha53.md 5.7K rendered
1# Nested app namespaces over one sliceable schema23Date: 2026-07-18\4Reviewed dependency: `jazz-tools@2.0.0-alpha.53`\5Decision: adopt `defineSliceableApp` into the facade with conformance coverage; adopt a lofi-owned6nested-namespace naming layer on top (issue #107, part 1)78## Question910Can one Jazz store host multiple app-level table namespaces (`taskapp.tasks` alongside11`notesapp.notes`) without a second schema, hash, or migration lineage — and can the schema facade12expose that shape without becoming the translating facade the13[facade decision](schema-facade-alpha53.md) rejects?1415## Context and control1617The control is the facade baseline: `s.defineApp` produces a single flat table namespace, and18`defineSliceableApp` was curated out as unexercised, with the recorded follow-up that it "joins the19surface when a use case and tests exist." The pinned alpha already ships the core primitive: one20compiled schema, typed sub-app views via `.slice(...)`. Slices select tables but do not rename them,21so nesting has to be a naming layer above the DSL, not a DSL change.2223## Decision2425- **`defineSliceableApp` joins the curated re-export.** The use case exists (nested namespaces build26 on it) and `package/schema/sliceable_test.ts` is the conformance coverage: slice-derived handles27 query the real engine through the one shared store, per-slice permissions compile and merge into28 one deployable bundle, and the migration surface works over a sliced schema. Facade member29 identity is unchanged — the member is the Jazz original.30- **A lofi-owned naming layer, clearly marked as such.** `s.defineNestedApp` flattens31 `taskapp.tasks` to the global name `taskapp__tasks` (the separator is reserved; it is not `.`32 because the permission builder reserves `${string}.${string}` keys for qualified-column `where`33 entries), compiles one schema through `defineSliceableApp`, and returns per-namespace apps with34 unprefixed typed handles. Handles are constructed exactly once, inside the call — the runtime keys35 table stores by handle identity, so the single-construction shape is the only expressible one. Ref36 targets written against namespace-local names (or `"<namespace>.<table>"` across namespaces) are37 rewritten to the mangled global names by cloning the column builders; the author's definition38 objects are never mutated.39- **Per-namespace permissions via `s.defineNestedPermissions`.** The pinned `definePermissions` keys40 compiled rules by the app object's property names and resolves foreign keys by real schema table41 names, so it cannot be handed a namespace with unprefixed keys without compiling table names the42 deployed schema does not contain. The lofi wrapper passes the mangled-key slice underneath and43 remaps only the policy context to local names: the policy context exposes exactly that namespace's44 tables, while compiled bundles stay keyed by the mangled global names and merge collision-free45 through `s.mergeNestedPermissions` (duplicates throw).46- **The runtime consumes a flattened table registry.** `runtimeTables()` previously walked47 `Object.values(schema)` one level deep, which would silently skip tables inside namespace objects48 during boot readiness and local→managed row migration. A nested root carries its flattened handle49 registry (the same handle objects the namespaces expose, so store identity is preserved); flat50 `defineApp` schemas keep the one-level walk.51- **Migrations are authored over the flattened schema.** `s.flattenNestedSchema` exposes the global52 definition; moving a table between namespaces is an ordinary `renameTableFrom` migration,53 conformance-verified.5455The nested members (`defineNestedApp`, `defineNestedPermissions`, `mergeNestedPermissions`,56`flattenNestedSchema`) are the one deliberate exception to the re-export rule: lofi-owned names for57a lofi-owned feature, pinned by test to be lofi's functions and to collide with no Jazz member. They58rename tables (the mangling); they do not reinterpret any Jazz semantics, so the translating facade59remains rejected.6061## Procedure and evidence6263- `package/schema/nested.ts` — the naming layer; `package/schema/nested_test.ts` — mangling,64 validation, ref rewriting, registry, and permissions-surface unit coverage.65- `package/schema/sliceable_test.ts` — `defineSliceableApp` conformance over the real engine (slice66 queries, merged per-slice permissions, migration over a sliced schema).67- `package/schema/nested_conformance_test.ts` — nested handles, per-namespace policy isolation,68 rewritten refs as working foreign keys, and the namespace-move migration over a real JazzServer.69- `package/runtime/runtime.ts` — `runtimeTables()` consumes the flattened registry.70- `deno task check` passes with the new suites wired into `test:unit` and `test:conformance`.7172## Contract delta7374The schema surface gains the nested-namespace members and `defineSliceableApp`; the facade promise75("expose the pinned DSL through the re-export, never a second schema language") is unchanged for76every Jazz member. Namespace and table names must not contain `__` or `.`.7778## Follow-up7980- G-set columns stay out of nested apps: under one store every table is a runtime sibling of every81 namespace, so the g-set isolation guidance (own single-table app) cannot be satisfied inside a82 nested app until the alpha.53 cross-table destabilization pin clears83 ([conformance findings](schema-facade-alpha53.md#conformance-findings-alpha53), finding 4).84 Documented in the [nested-namespaces example](../examples/nested-namespaces.md).85- Issue #107 parts 2 and 3 (runtime-declared sync location; recovery envelope carrying the sink)86 build on this layer and land separately.87- The ref-target rewrite clones builders via their internal `_targetTable`/`_element` fields;88 re-verify the shape on each alpha bump alongside the other pins.